Add android enterprise support for IoTS.

feature/appm-store/pbac
Dharmakeerthi Lasantha 5 years ago
commit 11610d3635

@ -0,0 +1,59 @@
/*
* Copyright (c) 2019, Entgra (Pvt) Ltd. (http://www.entgra.io) All Rights Reserved.
*
* Entgra (Pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.wso2.carbon.device.application.mgt.common.dto;
public class ApiRegistrationProfile {
private String applicationName;
private String tags[];
private boolean isAllowedToAllDomains;
private boolean isMappingAnExistingOAuthApp;
public String getApplicationName() {
return applicationName;
}
public void setApplicationName(String applicationName) {
this.applicationName = applicationName;
}
public String[] getTags() {
return tags;
}
public void setTags(String[] tags) {
this.tags = tags;
}
public boolean isAllowedToAllDomains() {
return isAllowedToAllDomains;
}
public void setAllowedToAllDomains(boolean allowedToAllDomains) {
isAllowedToAllDomains = allowedToAllDomains;
}
public boolean isMappingAnExistingOAuthApp() {
return isMappingAnExistingOAuthApp;
}
public void setMappingAnExistingOAuthApp(boolean mappingAnExistingOAuthApp) {
isMappingAnExistingOAuthApp = mappingAnExistingOAuthApp;
}
}

@ -94,6 +94,18 @@ public class ApplicationDTO {
required = true)
private List<ApplicationReleaseDTO> applicationReleaseDTOs;
@ApiModelProperty(name = "packageName",
value = "package name of the application")
private String packageName;
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public int getId() {
return id;
}

@ -0,0 +1,44 @@
package org.wso2.carbon.device.application.mgt.common.dto;
import org.wso2.carbon.device.mgt.common.DeviceIdentifier;
import java.util.List;
public class ApplicationPolicyDTO {
ApplicationDTO applicationDTO;
String policy;
List<DeviceIdentifier> deviceIdentifierList;
String action;
public List<DeviceIdentifier> getDeviceIdentifierList() {
return deviceIdentifierList;
}
public void setDeviceIdentifierList(List<DeviceIdentifier> deviceIdentifierList) {
this.deviceIdentifierList = deviceIdentifierList;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public ApplicationDTO getApplicationDTO() {
return applicationDTO;
}
public void setApplicationDTO(ApplicationDTO applicationDTO) {
this.applicationDTO = applicationDTO;
}
public String getPolicy() {
return policy;
}
public void setPolicy(String policy) {
this.policy = policy;
}
}

@ -82,6 +82,18 @@ public class Application {
required = true)
private List<ApplicationRelease> applicationReleases;
@ApiModelProperty(name = "packageName",
value = "package name of the application")
private String packageName;
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public int getId() { return id; }
public void setId(int id) { this.id = id; }

@ -273,4 +273,6 @@ public interface ApplicationManager {
*/
String getPlistArtifact(String uuid) throws ApplicationManagementException;
List<ApplicationReleaseDTO> getReleaseByPackageNames(List<String> packageIds) throws ApplicationManagementException;
}

@ -80,6 +80,10 @@
org.apache.commons.codec.digest;version="${commons-codec.wso2.osgi.version.range}",
org.wso2.carbon.base,
com.dd.*,
org.wso2.carbon.identity.jwt.client.extension.*,
org.wso2.carbon.apimgt.application.extension.*,
org.apache.commons.httpclient,
org.apache.commons.httpclient.methods,
org.apache.commons.validator.routines
</Import-Package>
<Embed-Dependency>apk-parser;scope=compile|runtime;inline=false</Embed-Dependency>
@ -240,6 +244,11 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wso2.carbon.devicemgt</groupId>
<artifactId>org.wso2.carbon.apimgt.application.extension</artifactId>
</dependency>
</dependencies>
</project>

@ -117,4 +117,7 @@ public interface ApplicationReleaseDAO {
boolean hasExistInstallableAppRelease(String releaseUuid, String installableStateName, int tenantId)
throws ApplicationManagementDAOException;
}
List<ApplicationReleaseDTO> getReleaseByPackages(List<String> packages, int tenantId)
throws ApplicationManagementDAOException;
}

@ -1,4 +1,4 @@
/*
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. licenses this file to you under the Apache License,
@ -36,8 +36,9 @@ import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
/**
/**
* GenericApplicationReleaseDAOImpl holds the implementation of ApplicationRelease related DAO operations.
*/
public class GenericApplicationReleaseDAOImpl extends AbstractDAOImpl implements ApplicationReleaseDAO {
@ -278,9 +279,15 @@ public class GenericApplicationReleaseDAOImpl extends AbstractDAOImpl implements
+ "APP_HASH_VALUE = ?, "
+ "SHARED_WITH_ALL_TENANTS = ?, "
+ "APP_META_INFO = ?, "
+ "SUPPORTED_OS_VERSIONS = ?, "
+ "CURRENT_STATE = ? "
+ "WHERE ID = ? AND TENANT_ID = ? ";
+ "SUPPORTED_OS_VERSIONS = ?";
if (applicationReleaseDTO.getCurrentState() != null) {
sql += ", CURRENT_STATE = ? ";
}
sql += " WHERE ID = ? AND TENANT_ID = ? ";
int x = 17;
try {
Connection connection = this.getDBConnection();
try (PreparedStatement statement = connection.prepareStatement(sql)) {
@ -300,9 +307,12 @@ public class GenericApplicationReleaseDAOImpl extends AbstractDAOImpl implements
statement.setBoolean(14, applicationReleaseDTO.getIsSharedWithAllTenants());
statement.setString(15, applicationReleaseDTO.getMetaData());
statement.setString(16, applicationReleaseDTO.getSupportedOsVersions());
statement.setString(17, applicationReleaseDTO.getCurrentState().toUpperCase());
statement.setInt(18, applicationReleaseDTO.getId());
statement.setInt(19, tenantId);
if (applicationReleaseDTO.getCurrentState() != null) {
statement.setString(x++, applicationReleaseDTO.getCurrentState().toUpperCase());
}
statement.setInt(x++, applicationReleaseDTO.getId());
statement.setInt(x++, tenantId);
if (statement.executeUpdate() == 0) {
return null;
}
@ -550,4 +560,65 @@ public class GenericApplicationReleaseDAOImpl extends AbstractDAOImpl implements
throw new ApplicationManagementDAOException(msg, e);
}
}
@Override
public List<ApplicationReleaseDTO> getReleaseByPackages(List<String> packages, int tenantId) throws
ApplicationManagementDAOException {
String sql = "SELECT "
+ "AR.ID AS RELEASE_ID, "
+ "AR.DESCRIPTION AS RELEASE_DESCRIPTION, "
+ "AR.VERSION AS RELEASE_VERSION, "
+ "AR.UUID AS RELEASE_UUID, "
+ "AR.RELEASE_TYPE AS RELEASE_TYPE, "
+ "AR.INSTALLER_LOCATION AS AP_RELEASE_STORED_LOC, "
+ "AR.ICON_LOCATION AS AP_RELEASE_ICON_LOC, "
+ "AR.BANNER_LOCATION AS AP_RELEASE_BANNER_LOC, "
+ "AR.SC_1_LOCATION AS AP_RELEASE_SC1, "
+ "AR.SC_2_LOCATION AS AP_RELEASE_SC2, "
+ "AR.SC_3_LOCATION AS AP_RELEASE_SC3, "
+ "AR.APP_HASH_VALUE AS RELEASE_HASH_VALUE, "
+ "AR.APP_PRICE AS RELEASE_PRICE, "
+ "AR.APP_META_INFO AS RELEASE_META_INFO, "
+ "AR.PACKAGE_NAME AS PACKAGE_NAME, "
+ "AR.SUPPORTED_OS_VERSIONS AS RELEASE_SUP_OS_VERSIONS, "
+ "AR.RATING AS RELEASE_RATING, "
+ "AR.CURRENT_STATE AS RELEASE_CURRENT_STATE, "
+ "AR.RATED_USERS AS RATED_USER_COUNT "
+ "FROM AP_APP_RELEASE AS AR "
+ "WHERE AR.TENANT_ID = ? AND AR.PACKAGE_NAME IN (";
StringJoiner joiner = new StringJoiner(",", sql, ")");
packages.stream().map(ignored -> "?").forEach(joiner::add);
sql = joiner.toString();
try {
Connection connection = this.getDBConnection();
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, tenantId);
for (int y = 0; y < packages.size(); y++) {
// y +2 because tenantId parameter is 1 and the counter is starting at o for y
statement.setString(y+2, packages.get(y));
}
try (ResultSet resultSet = statement.executeQuery()) {
List<ApplicationReleaseDTO> releaseDTOs = new ArrayList<>();
while (resultSet.next()) {
releaseDTOs.add(DAOUtil.constructAppReleaseDTO(resultSet));
}
return releaseDTOs;
}
}
} catch (DBConnectionException e) {
String msg = "Database connection error occurred while trying to get application release details which has "
+ "packages: " + String.join(", ", packages);
log.error(msg, e);
throw new ApplicationManagementDAOException(msg, e);
} catch (SQLException e) {
String msg =
"Error while getting application release details which has packages: " + String.join(", ", packages)
+ " , while executing the query " + sql;
log.error(msg, e);
throw new ApplicationManagementDAOException(msg, e);
}
}
}

@ -176,13 +176,15 @@ public class ApplicationManagerImpl implements ApplicationManager {
@Override
public Application createPublicApp(PublicAppWrapper publicAppWrapper, ApplicationArtifact applicationArtifact)
throws ApplicationManagementException {
int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(true);
if (log.isDebugEnabled()) {
log.debug("Public app creating request is received. App name: " + publicAppWrapper.getName()
+ " Device Type: " + publicAppWrapper.getDeviceType());
}
String publicAppStorePath = "";
if (DeviceTypes.ANDROID.toString().equals(publicAppWrapper.getDeviceType())) {
if (DeviceTypes.ANDROID.toString().toLowerCase().equals(publicAppWrapper.getDeviceType())) {
publicAppStorePath = Constants.GOOGLE_PLAY_STORE_URL;
} else if (DeviceTypes.IOS.toString().equals(publicAppWrapper.getDeviceType())) {
publicAppStorePath = Constants.APPLE_STORE_URL;
@ -194,11 +196,50 @@ public class ApplicationManagerImpl implements ApplicationManager {
applicationReleaseDTO.setInstallerName(appInstallerUrl);
applicationReleaseDTO.setUuid(UUID.randomUUID().toString());
applicationReleaseDTO.setAppHashValue(DigestUtils.md5Hex(appInstallerUrl));
ConnectionManagerUtil.openDBConnection();
List<ApplicationReleaseDTO> exitingRelease;
try {
exitingRelease = applicationReleaseDAO.getReleaseByPackages(Arrays.asList(applicationReleaseDTO.getPackageName())
, tenantId);
} catch (ApplicationManagementDAOException e) {
String msg = "Error Occured when fetching release: " + publicAppWrapper.getName();
log.error(msg);
throw new ApplicationManagementException(msg, e);
} finally {
ConnectionManagerUtil.closeDBConnection();
}
if (exitingRelease != null && exitingRelease.size() > 0) {
applicationDTO.getApplicationReleaseDTOs().clear();
applicationReleaseDTO.setUuid(exitingRelease.get(0).getUuid());
applicationReleaseDTO.setCurrentState(exitingRelease.get(0).getCurrentState());
try {
applicationReleaseDTO = addImageArtifacts(applicationReleaseDTO, applicationArtifact);
applicationDTO.getApplicationReleaseDTOs().add(applicationReleaseDTO);
ConnectionManagerUtil.beginDBTransaction();
applicationReleaseDAO.updateRelease(applicationReleaseDTO, tenantId);
ConnectionManagerUtil.commitDBTransaction();
return APIUtil.appDtoToAppResponse(applicationDTO);
} catch (ApplicationManagementDAOException e) {
ConnectionManagerUtil.rollbackDBTransaction();
String msg = "Error occurred when updating public app: " + publicAppWrapper.getName();
log.error(msg, e);
throw new ApplicationManagementException(msg, e);
} catch (ResourceManagementException e) {
String msg = "Error occurred when adding artifacts of release: " + publicAppWrapper.getName();
log.error(msg, e);
throw new ApplicationManagementException(msg, e);
} finally {
ConnectionManagerUtil.closeDBConnection();
}
} else {
//uploading application artifacts
try {
applicationReleaseDTO = addImageArtifacts(applicationReleaseDTO, applicationArtifact);
applicationDTO.getApplicationReleaseDTOs().clear();
applicationDTO.getApplicationReleaseDTOs()
.add(addImageArtifacts(applicationReleaseDTO, applicationArtifact));
applicationDTO.getApplicationReleaseDTOs().add(applicationReleaseDTO);
} catch (ResourceManagementException e) {
String msg = "Error Occured when uploading artifacts of the public app: " + publicAppWrapper.getName();
log.error(msg, e);
@ -208,6 +249,8 @@ public class ApplicationManagerImpl implements ApplicationManager {
return addAppDataIntoDB(applicationDTO);
}
}
@Override
public Application createCustomApp(CustomAppWrapper customAppWrapper, ApplicationArtifact applicationArtifact)
throws ApplicationManagementException {
@ -3115,4 +3158,24 @@ public class ApplicationManagerImpl implements ApplicationManager {
ConnectionManagerUtil.closeDBConnection();
}
}
@Override
public List<ApplicationReleaseDTO> getReleaseByPackageNames(List<String> packageIds) throws ApplicationManagementException {
int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(true);
try {
ConnectionManagerUtil.openDBConnection();
return this.applicationReleaseDAO.getReleaseByPackages(packageIds, tenantId);
} catch (DBConnectionException e) {
String msg = "Error occurred while obtaining the database connection for getting application for the " +
"packages";
log.error(msg, e);
throw new ApplicationManagementException(msg, e);
} catch (ApplicationManagementDAOException e) {
String msg = "Error occurred while getting application data for packages";
log.error(msg, e);
throw new ApplicationManagementException(msg, e);
} finally {
ConnectionManagerUtil.closeDBConnection();
}
}
}

@ -17,9 +17,19 @@
package org.wso2.carbon.device.application.mgt.core.impl;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.apimgt.application.extension.dto.ApiApplicationKey;
import org.wso2.carbon.apimgt.application.extension.exception.APIManagerException;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.device.application.mgt.common.ApplicationInstallResponse;
import org.wso2.carbon.device.application.mgt.common.ApplicationType;
@ -29,6 +39,7 @@ import org.wso2.carbon.device.application.mgt.common.SubAction;
import org.wso2.carbon.device.application.mgt.common.SubscriptionType;
import org.wso2.carbon.device.application.mgt.common.SubscribingDeviceIdHolder;
import org.wso2.carbon.device.application.mgt.common.dto.ApplicationDTO;
import org.wso2.carbon.device.application.mgt.common.dto.ApplicationPolicyDTO;
import org.wso2.carbon.device.application.mgt.common.dto.DeviceSubscriptionDTO;
import org.wso2.carbon.device.application.mgt.common.dto.ScheduledSubscriptionDTO;
import org.wso2.carbon.device.application.mgt.common.exception.ApplicationManagementException;
@ -51,6 +62,7 @@ import org.wso2.carbon.device.application.mgt.core.util.APIUtil;
import org.wso2.carbon.device.application.mgt.core.util.ConnectionManagerUtil;
import org.wso2.carbon.device.application.mgt.core.util.Constants;
import org.wso2.carbon.device.application.mgt.core.util.HelperUtil;
import org.wso2.carbon.device.application.mgt.core.util.OAuthUtils;
import org.wso2.carbon.device.mgt.common.Device;
import org.wso2.carbon.device.mgt.common.DeviceIdentifier;
import org.wso2.carbon.device.mgt.common.MDMAppConstants;
@ -65,13 +77,18 @@ import org.wso2.carbon.device.mgt.common.operation.mgt.Activity;
import org.wso2.carbon.device.mgt.common.operation.mgt.ActivityStatus;
import org.wso2.carbon.device.mgt.common.operation.mgt.Operation;
import org.wso2.carbon.device.mgt.common.operation.mgt.OperationManagementException;
import org.wso2.carbon.device.mgt.common.policy.mgt.ProfileFeature;
import org.wso2.carbon.device.mgt.core.dto.DeviceType;
import org.wso2.carbon.device.mgt.core.operation.mgt.ProfileOperation;
import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService;
import org.wso2.carbon.device.mgt.core.service.GroupManagementProviderService;
import org.wso2.carbon.device.mgt.core.util.MDMAndroidOperationUtil;
import org.wso2.carbon.device.mgt.core.util.MDMIOSOperationUtil;
import org.wso2.carbon.identity.jwt.client.extension.dto.AccessTokenInfo;
import org.wso2.carbon.user.api.UserStoreException;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@ -376,10 +393,21 @@ public class SubscriptionManagerImpl implements SubscriptionManager {
entry.getKey(), action);
activityList.add(activity);
}
} else {
if (applicationDTO.getType().equals(ApplicationType.PUBLIC.toString())) {
List<String> categories = getApplicationCategories(applicationDTO.getId());
if (categories.contains("GooglePlaySyncedApp")) {
ApplicationPolicyDTO applicationPolicyDTO = new ApplicationPolicyDTO();
applicationPolicyDTO.setApplicationDTO(applicationDTO);
applicationPolicyDTO.setDeviceIdentifierList(deviceIdentifiers);
applicationPolicyDTO.setAction(action);
installEnrollmentApplications(applicationPolicyDTO);
}
} else {
Activity activity = addAppOperationOnDevices(applicationDTO, deviceIdentifiers, deviceType, action);
activityList.add(activity);
}
}
ApplicationInstallResponse applicationInstallResponse = new ApplicationInstallResponse();
applicationInstallResponse.setActivities(activityList);
applicationInstallResponse.setIgnoredDeviceIdentifiers(ignoredDeviceIdentifiers);
@ -414,6 +442,22 @@ public class SubscriptionManagerImpl implements SubscriptionManager {
return subscribingDeviceIdHolder;
}
private List<String> getApplicationCategories(int id) throws ApplicationManagementException {
List<String> categories;
int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(true);
try {
ConnectionManagerUtil.openDBConnection();
categories = this.applicationDAO.getAppCategories(id, tenantId);
return categories;
} catch (ApplicationManagementDAOException e) {
String msg = "Error occurred while getting categories for application : " + id;
log.error(msg, e);
throw new ApplicationManagementException(msg);
} finally {
ConnectionManagerUtil.closeDBConnection();
}
}
private ApplicationDTO getApplicationDTO(String uuid) throws ApplicationManagementException {
ApplicationDTO applicationDTO;
int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(true);
@ -681,4 +725,55 @@ public class SubscriptionManagerImpl implements SubscriptionManager {
throw new ApplicationManagementException(msg, e);
}
}
public int installEnrollmentApplications(ApplicationPolicyDTO applicationPolicyDTO)
throws ApplicationManagementException {
HttpClient httpClient;
PostMethod request;
try {
String tenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain();
ApiApplicationKey apiApplicationKey = OAuthUtils.getClientCredentials(tenantDomain);
String username = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm()
.getRealmConfiguration().getAdminUserName() + Constants.ApplicationInstall.AT + tenantDomain;
AccessTokenInfo tokenInfo = OAuthUtils.getOAuthCredentials(apiApplicationKey, username);
String requestUrl = Constants.ApplicationInstall.ENROLLMENT_APP_INSTALL_PROTOCOL +
System.getProperty(Constants.ApplicationInstall.IOT_CORE_HOST) +
Constants.ApplicationInstall.COLON +
System.getProperty(Constants.ApplicationInstall.IOT_CORE_PORT) +
Constants.ApplicationInstall.GOOGLE_APP_INSTALL_URL;
Gson gson = new Gson();
String payload = gson.toJson(applicationPolicyDTO);
StringRequestEntity requestEntity = new StringRequestEntity(payload, MediaType.APPLICATION_JSON
, Constants.ApplicationInstall.ENCODING);;
httpClient = new HttpClient();
request = new PostMethod(requestUrl);
request.addRequestHeader(Constants.ApplicationInstall.AUTHORIZATION
, Constants.ApplicationInstall.AUTHORIZATION_HEADER_VALUE + tokenInfo.getAccessToken());
request.setRequestEntity(requestEntity);
httpClient.executeMethod(request);
return request.getStatusCode();
} catch (UserStoreException e) {
String msg = "Error while accessing user store for user with Android device.";
log.error(msg, e);
throw new ApplicationManagementException(msg, e);
} catch (APIManagerException e) {
String msg = "Error while retrieving access token for Android device" ;
log.error(msg, e);
throw new ApplicationManagementException(msg, e);
} catch (HttpException e) {
String msg = "Error while calling the app store to install enrollment app with id: " +
applicationPolicyDTO.getApplicationDTO().getId() +
" on device";
log.error(msg, e);
throw new ApplicationManagementException(msg, e);
} catch (IOException e) {
String msg = "Error while installing the enrollment with id: " + applicationPolicyDTO.getApplicationDTO()
.getId() + " on device";
log.error(msg, e);
throw new ApplicationManagementException(msg, e);
}
}
}

@ -356,6 +356,7 @@ public class APIUtil {
applicationReleases.add(releaseDtoToRelease(applicationReleaseDTO));
}
application.setApplicationReleases(applicationReleases);
application.setPackageName(applicationDTO.getPackageName());
return application;
}

@ -115,4 +115,30 @@ public class Constants {
public static final int REVIEW_PARENT_ID = -1;
public static final int MAX_RATING = 5;
public final class ApplicationInstall {
private ApplicationInstall() {
throw new AssertionError();
}
public static final String APPLICATION_NAME = "device_type_android";
public static final String ENROLLMENT_APP_INSTALL_FEATURE_CODE = "ENROLLMENT_APP_INSTALL";
public static final String DEFAULT_TOKEN_TYPE = "PRODUCTION";
public static final String DEFAULT_VALIDITY_PERIOD = "3600";
public static final String SUBSCRIPTION_SCOPE = "appm:subscribe";
public static final String ENROLLMENT_APP_INSTALL_UUID = "uuid";
public static final String GOOGLE_POLICY_PAYLOAD = "installGooglePolicyPayload";
public static final String ENROLLMENT_APP_INSTALL_CODE = "enrollmentAppInstall";
public static final String ENCODING = "UTF-8";
public static final String AT = "@";
public static final String DEVICE_TYPE_ANDROID = "android";
public static final String COLON = ":";
public static final String IOT_CORE_HOST = "iot.core.host";
public static final String IOT_CORE_PORT = "iot.core.https.port";
public static final String ENROLLMENT_APP_INSTALL_PROTOCOL = "https://";
public static final String GOOGLE_APP_INSTALL_URL = "/api/device-mgt/android/v1.0/enterprise/change-app";
public static final String AUTHORIZATION = "Authorization";
public static final String AUTHORIZATION_HEADER_VALUE = "Bearer ";
}
}

@ -105,6 +105,7 @@ public class DAOUtil {
application.setStatus(rs.getString("APP_STATUS"));
application.setAppRating(rs.getDouble("APP_RATING"));
application.setDeviceTypeId(rs.getInt("APP_DEVICE_TYPE_ID"));
application.setPackageName(rs.getString("PACKAGE_NAME"));
application.getApplicationReleaseDTOs().add(constructAppReleaseDTO(rs));
} else {
if (application != null && application.getApplicationReleaseDTOs() != null) {

@ -0,0 +1,90 @@
/*
* Copyright (c) 2019, Entgra (Pvt) Ltd. (http://www.entgra.io) All Rights Reserved.
*
* Entgra (Pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.wso2.carbon.device.application.mgt.core.util;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.apimgt.application.extension.APIManagementProviderService;
import org.wso2.carbon.apimgt.application.extension.dto.ApiApplicationKey;
import org.wso2.carbon.apimgt.application.extension.exception.APIManagerException;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.device.application.mgt.common.dto.ApiRegistrationProfile;
import org.wso2.carbon.identity.jwt.client.extension.JWTClient;
import org.wso2.carbon.identity.jwt.client.extension.dto.AccessTokenInfo;
import org.wso2.carbon.identity.jwt.client.extension.exception.JWTClientException;
import org.wso2.carbon.identity.jwt.client.extension.service.JWTClientManagerService;
import org.wso2.carbon.user.api.UserStoreException;
import org.wso2.carbon.utils.multitenancy.MultitenantConstants;
public class OAuthUtils {
private static final Log log = LogFactory.getLog(OAuthUtils.class);
public static ApiApplicationKey getClientCredentials(String tenantDomain)
throws UserStoreException, APIManagerException {
ApiRegistrationProfile registrationProfile = new ApiRegistrationProfile();
registrationProfile.setApplicationName(Constants.ApplicationInstall.APPLICATION_NAME);
registrationProfile.setTags(new String[]{Constants.ApplicationInstall.DEVICE_TYPE_ANDROID});
registrationProfile.setAllowedToAllDomains(false);
registrationProfile.setMappingAnExistingOAuthApp(false);
return getCredentials(registrationProfile, tenantDomain);
}
public static ApiApplicationKey getCredentials(ApiRegistrationProfile registrationProfile, String tenantDomain)
throws UserStoreException, APIManagerException {
ApiApplicationKey apiApplicationKeyInfo;
if (tenantDomain == null || tenantDomain.isEmpty()) {
tenantDomain = MultitenantConstants.SUPER_TENANT_DOMAIN_NAME;
}
try {
PrivilegedCarbonContext.startTenantFlow();
PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain(tenantDomain, true);
PrivilegedCarbonContext.getThreadLocalCarbonContext().setUsername(PrivilegedCarbonContext.
getThreadLocalCarbonContext().getUserRealm().getRealmConfiguration().getAdminUserName());
PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext();
APIManagementProviderService apiManagementProviderService = (APIManagementProviderService) ctx.
getOSGiService(APIManagementProviderService.class, null);
apiApplicationKeyInfo = apiManagementProviderService.
generateAndRetrieveApplicationKeys(registrationProfile.getApplicationName(),
registrationProfile.getTags(), Constants.ApplicationInstall.DEFAULT_TOKEN_TYPE,
registrationProfile.getApplicationName(), registrationProfile.isAllowedToAllDomains(),
Constants.ApplicationInstall.DEFAULT_VALIDITY_PERIOD);
} finally {
PrivilegedCarbonContext.endTenantFlow();
}
return apiApplicationKeyInfo;
}
public static AccessTokenInfo getOAuthCredentials(ApiApplicationKey apiApplicationKey, String username)
throws APIManagerException {
try {
PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext();
JWTClientManagerService jwtClientManagerService = (JWTClientManagerService) ctx.
getOSGiService(JWTClientManagerService.class, null);
JWTClient jwtClient = jwtClientManagerService.getJWTClient();
return jwtClient.getAccessToken(apiApplicationKey.getConsumerKey(), apiApplicationKey.getConsumerSecret(),
username, Constants.ApplicationInstall.SUBSCRIPTION_SCOPE);
} catch (JWTClientException e) {
String errorMsg = "Error while generating an OAuth token for user " + username;
log.error(errorMsg, e);
throw new APIManagerException(errorMsg, e);
}
}
}

@ -10,13 +10,16 @@
},
"license": "Apache License 2.0",
"dependencies": {
"@ant-design/dark-theme": "^0.2.2",
"@babel/polyfill": "^7.6.0",
"acorn": "^6.2.0",
"antd": "^3.20.1",
"antd": "^3.22.2",
"axios": "^0.19.0",
"d3": "^5.9.7",
"dagre": "^0.8.4",
"fetch": "^1.1.0",
"gapi": "0.0.3",
"gapi-client": "0.0.3",
"keymirror": "^0.1.1",
"rc-tween-one": "^2.4.1",
"react-d3-graph": "^2.1.0",
@ -54,7 +57,7 @@
"html-webpack-plugin": "^3.2.0",
"img-loader": "^3.0.1",
"json-loader": "^0.5.7",
"less": "^3.9.0",
"less": "^3.10.3",
"less-loader": "^4.1.0",
"mini-css-extract-plugin": "^0.5.0",
"mocha": "^5.2.0",
@ -73,8 +76,8 @@
"sass-loader": "^6.0.7",
"style-loader": "^0.18.2",
"url-loader": "^1.1.2",
"webpack": "^4.35.2",
"webpack-cli": "^3.3.5",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.7",
"webpack-dev-server": "^3.7.2"
},
"scripts": {

@ -1,14 +1,9 @@
{
"theme": {
"type": "default",
"value": "lightBaseTheme",
"logo": "https://entgra.io/assets/images/svg/logo.svg",
"primaryColor": "rgb(24, 144, 255)"
"primaryColor": "#badc58"
},
"serverConfig": {
"protocol": "https",
"hostname": "localhost",
"httpsPort": "9443",
"invoker": {
"uri": "/publisher-ui-request-handler/invoke",
"publisher": "/application-mgt-publisher/v1.0",

@ -67,18 +67,37 @@ class App extends React.Component {
axios.get(
window.location.origin + "/publisher/public/conf/config.json",
).then(res => {
console.log(res);
const config = res.data;
this.getAndroidEnterpriseToken(config);
}).catch((error) => {
this.setState({
loading: false,
config: res.data
error: true
})
});
}
getAndroidEnterpriseToken = (config) => {
axios.get(
window.location.origin + config.serverConfig.invoker.uri + "/device-mgt/android/v1.0/enterprise/store-url?approveApps=true" +
"&searchEnabled=true&isPrivateAppsEnabled=true&isWebAppEnabled=true&isOrganizeAppPageVisible=true&isManagedConfigEnabled=true" +
"&host=https://localhost:9443",
).then(res => {
config.androidEnterpriseToken = res.data.data.token;
this.setState({
loading: false,
config: config
});
}).catch((error) => {
config.androidEnterpriseToken = null;
this.setState({
loading: false,
error: true
config: config
})
});
}
};
render() {
const {loading, error} = this.state;

@ -40,6 +40,8 @@ import ReactHtmlParser from 'react-html-parser';
import "./AppDetailsDrawer.css";
import pSBC from "shade-blend-color";
import {withConfigContext} from "../../../../context/ConfigContext";
import ManagedConfigurationsIframe
from "../../../manage/android-enterprise/ManagedConfigurationsIframe/ManagedConfigurationsIframe";
const {Meta} = Card;
const {Text, Title} = Typography;
@ -474,6 +476,31 @@ class AppDetailsDrawer extends React.Component {
<Divider/>
{/*display manage config button only if the app is public android app*/}
{(app.type === "PUBLIC") && (app.deviceType === "android") && (
<div>
<div>
<Text strong={true}>Set up managed configurations</Text>
</div>
<div style={{paddingTop: 16}}>
<Text>
If you are developing apps for the enterprise market, you may need to satisfy
particular requirements set by a organization's policies. Managed
configurations,
previously known as application restrictions, allow the organization's IT admin
to
remotely specify settings for apps. This capability is particularly useful for
organization-approved apps deployed to a work profile.
</Text>
</div>
<br/>
<ManagedConfigurationsIframe
style={{paddingTop: 16}}
packageName={app.packageName}/>
<Divider dashed={true}/>
</div>
)}
<Text strong={true}>Releases </Text>
{/*display add new release only if app type is enterprise*/}

@ -224,7 +224,7 @@ class EditReleaseModal extends React.Component {
isSharedWithAllTenants,
metaData: JSON.stringify(this.state.metaData),
releaseType: releaseType,
supportedOsVersions: "4.0-10.0"
supportedOsVersions: "4-30"
};
if (specificElements.hasOwnProperty("binaryFile") && binaryFiles.length === 1) {

@ -0,0 +1,125 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Button, Divider, Form, Input, message, Modal, notification, Spin} from "antd";
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
import {withRouter} from "react-router";
import {handleApiError} from "../../../../js/Utils";
class AddNewPage extends React.Component {
state = {
visible: false,
pageName: ''
};
showModal = () => {
this.setState({
visible: true,
loading: false
});
};
handleCancel = e => {
console.log(e);
this.setState({
visible: false,
});
};
handlePageName = (e) => {
this.setState({
pageName: e.target.value,
});
};
createNewPage = () => {
const config = this.props.context;
this.setState({loading: true});
axios.post(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/page",
{
"locale": "en",
"pageName": this.state.pageName
}
).then(res => {
if (res.status === 200) {
const {pageId, pageName} = res.data.data;
notification["success"]({
message: 'Saved!',
description: 'Page created successfully!'
});
this.setState({loading: false});
this.props.history.push(`/publisher/manage/android-enterprise/pages/${pageName}/${pageId}`);
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to update the cluster.");
this.setState({loading: false});
});
};
render() {
return (
<div style={{marginTop: 24, marginBottom: 24}}>
<Button
type="dashed"
onClick={this.showModal}>
Add new page
</Button>
<Modal
title="Add new page"
visible={this.state.visible}
onOk={this.createNewPage}
onCancel={this.handleCancel}
okText="Create Page"
footer={null}
>
<Spin spinning={this.state.loading}>
<p>Choose a name for the page</p>
<Input onChange={this.handlePageName}/>
<Divider/>
<div>
<Button
onClick={this.handleCancel}>
Cancel
</Button>
<Divider type="vertical"/>
<Button
onClick={this.createNewPage}
htmlType="button" type="primary"
disabled={this.state.pageName.length === 0}>
Create Page
</Button>
</div>
</Spin>
</Modal>
</div>
);
}
}
export default withConfigContext(withRouter(AddNewPage));

@ -0,0 +1,81 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Modal, Button} from "antd";
import {withConfigContext} from "../../../context/ConfigContext";
class GooglePlayIframe extends React.Component {
constructor(props) {
super(props);
this.config = this.props.context;
this.state = {
visible: false
};
}
showModal = () => {
this.setState({
visible: true,
});
};
handleOk = e => {
console.log(e);
this.setState({
visible: false,
});
};
handleCancel = e => {
console.log(e);
this.setState({
visible: false,
});
};
render() {
return (
<div style={{display: "inline-block", padding: 4}}>
<Button type="primary" onClick={this.showModal}>
Approve Applications
</Button>
<Modal
title={null}
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
width = {740}
footer={null}>
<iframe
style={{
height: 720,
border: 0,
width: "100%"
}}
src={"https://play.google.com/work/embedded/search?token=" + this.config.androidEnterpriseToken +
"&mode=APPROVE&showsearchbox=TRUE"}
/>
</Modal>
</div>
);
}
}
export default withConfigContext(GooglePlayIframe);

@ -0,0 +1,224 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Button, message, Modal, notification, Spin} from "antd";
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
import {handleApiError} from "../../../../js/Utils";
// import gapi from 'gapi-client';
class ManagedConfigurationsIframe extends React.Component {
constructor(props) {
super(props);
this.config = this.props.context;
this.state = {
visible: false,
loading: false
};
}
showModal = () => {
this.getMcm();
this.setState({
visible: true,
});
};
handleOk = e => {
console.log(e);
this.setState({
visible: false,
});
};
handleCancel = e => {
console.log(e);
this.setState({
visible: false,
});
};
getMcm = () => {
const {packageName} = this.props;
this.setState({loading: true});
//send request to the invoker
axios.get(
window.location.origin + this.config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/managed-configs/package/" + packageName,
).then(res => {
if (res.status === 200) {
let mcmId = null;
if (res.data.hasOwnProperty("data")) {
mcmId = res.data.data.mcmId;
}
this.loadIframe(mcmId);
this.setState({loading: false});
}
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin + '/publisher/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load configurations.",
});
}
this.setState({loading: false, visible: false});
});
};
loadIframe = (mcmId) => {
const {packageName} = this.props;
let method = "post";
gapi.load('gapi.iframes', () => {
const parameters = {
token: this.config.androidEnterpriseToken,
packageName: packageName
};
if (mcmId != null) {
parameters.mcmId = mcmId;
parameters.canDelete = true;
method = "put";
}
const queryString = Object.keys(parameters).map(key => key + '=' + parameters[key]).join('&');
var options = {
'url': "https://play.google.com/managed/mcm?" + queryString,
'where': document.getElementById('manage-config-iframe-container'),
'attributes': {style: 'height:720px', scrolling: 'yes'}
};
var iframe = gapi.iframes.getContext().openChild(options);
iframe.register('onconfigupdated', (event) => {
this.updateConfig(method, event);
}, gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER);
iframe.register('onconfigdeleted', (event) => {
this.deleteConfig(event);
}, gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER);
});
};
updateConfig = (method, event) => {
const {packageName} = this.props;
this.setState({loading: true});
console.log(event);
const data = {
mcmId: event.mcmId,
profileName: event.name,
packageName
};
//send request to the invoker
axios({
method,
url: window.location.origin + this.config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/managed-configs",
data
}).then(res => {
if (res.status === 200 || res.status === 201) {
notification["success"]({
message: 'Saved!',
description: 'Configuration Profile updated Successfully',
});
this.setState({
loading: false,
visible: false
});
}
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin + '/publisher/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to update configurations.",
});
}
this.setState({loading: false});
});
};
deleteConfig = (event) => {
const {packageName} = this.props;
this.setState({loading: true});
console.log(event);
//send request to the invoker
axios.delete(
window.location.origin + this.config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/managed-configs/mcm/" + event.mcmId
).then(res => {
if (res.status === 200 || res.status === 201) {
notification["success"]({
message: 'Saved!',
description: 'Configuration Profile removed Successfully',
});
this.setState({
loading: false,
visible: false
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to remove configurations.");
this.setState({loading: false});
});
};
render() {
return (
<div>
<Button
size="small"
type="primary"
icon="setting"
onClick={this.showModal}>
Manage
</Button>
<Modal
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
footer={null}>
<Spin spinning={this.state.loading}>
<div id="manage-config-iframe-container">
</div>
</Spin>
</Modal>
</div>
);
}
}
export default withConfigContext(ManagedConfigurationsIframe);

@ -0,0 +1,122 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Modal, Icon, Table, Avatar} from 'antd';
import "../Cluster.css";
import {withConfigContext} from "../../../../../../context/ConfigContext";
const columns = [
{
title: '',
dataIndex: 'iconUrl',
key: 'iconUrl',
render: (iconUrl) => (<Avatar shape="square" src={iconUrl}/>)
},
{
title: 'Name',
dataIndex: 'name',
key: 'name'
},
{
title: 'Page',
dataIndex: 'packageId',
key: 'packageId'
}
];
class AddAppsToClusterModal extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
loading: false,
selectedProducts: [],
homePageId: null
};
}
showModal = () => {
this.setState({
visible: true,
});
};
handleOk = () => {
this.props.addSelectedProducts(this.state.selectedProducts);
this.handleCancel();
};
handleCancel = () => {
this.setState({
visible: false,
});
};
rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
this.setState({
selectedProducts: selectedRows
})
},
};
render() {
const {pagination, loading} = this.state;
return (
<div>
<div className="btn-add-new-wrapper">
<div className="btn-add-new">
<button className="btn"
onClick={this.showModal}>
<Icon style={{position: "relative"}} type="plus"/>
</button>
</div>
<div className="title">
Add app
</div>
</div>
<Modal
title="Select Apps"
width={640}
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}>
<Table
columns={columns}
rowKey={record => record.packageId}
dataSource={this.props.unselectedProducts}
scroll={{ x: 300 }}
pagination={{
...pagination,
size: "small",
// position: "top",
showTotal: (total, range) => `showing ${range[0]}-${range[1]} of ${total} pages`,
showQuickJumper: true
}}
loading={loading}
onChange={this.handleTableChange}
rowSelection={this.rowSelection}
/>
</Modal>
</div>
);
}
}
export default withConfigContext(AddAppsToClusterModal);

@ -0,0 +1,168 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.cluster{
border-radius: 5px;
background-color: white;
padding: 24px;
margin: 14px 0 14px 0;
}
.cluster .products-row{
-webkit-align-items: center;
align-items: center;
display: -webkit-box;
display: -webkit-flex;
display: flex;
-webkit-flex-wrap: wrap;
flex-wrap: wrap;
margin-top: 24px;
}
.cluster .product{
display: -webkit-box;
display: -webkit-flex;
display: flex;
padding: 20px 0;
position: relative;
}
.cluster .product .product-icon{
display: -webkit-box;
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
height: 180px;
width: 90px;
}
.cluster .product .title, .cluster .btn-add-new-wrapper .title {
color: #202124;
overflow: hidden;
position: relative;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 16px;
}
.cluster .product .product-icon img{
-webkit-align-self: center;
align-self: center;
height: 90px;
/*padding-bottom: 16px;*/
width: 90px;
border-radius: 28%;
}
.cluster .product .arrow {
color: #80868b;
font-size: 20px;
position: relative;
top: 20px;
width: 36px;
}
.cluster .product .arrow .btn {
width: 48px;
height: 48px;
padding: 12px;
font-size: 24px;
display: inline-block;
position: relative;
-webkit-box-sizing: border-box;
box-sizing: border-box;
border: none;
outline: none;
background-color: transparent;
fill: currentColor;
color: inherit;
text-decoration: none;
cursor: pointer;
-webkit-user-select: none;
}
.cluster .product .arrow .btn-right {
left: -12px;
}
.cluster .product .delete-btn {
color: #80868b;
font-size: 20px;
position: absolute;
right: 0;
top: -10px;
}
.cluster .product .delete-btn .btn {
width: 48px;
height: 48px;
padding: 16px 28px 0 0;
font-size: 24px;
display: inline-block;
position: relative;
-webkit-box-sizing: border-box;
box-sizing: border-box;
border: none;
outline: none;
background-color: transparent;
fill: currentColor;
color: inherit;
text-decoration: none;
cursor: pointer;
-webkit-user-select: none;
}
.cluster .btn-add-new-wrapper{
display: -webkit-box;
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
height: 180px;
width: 90px;
margin: 0 33px;
}
.cluster .btn-add-new{
-webkit-align-self: center;
align-self: center;
height: 90px;
padding-bottom: 16px;
width: 90px;
border-radius: 28%;
}
.cluster .btn-add-new .btn{
height: 36px;
padding: 0 23px 0 23px;
border-width: 1px;
min-height: 90px;
width: 90px;
background-color: transparent;
border-radius: 28%;
}
.cluster .btn-add-new :hover{
background-color: rgba(250, 159, 0, 0.2);
cursor: pointer;
}
.cluster .ant-typography-edit-content{
width: 200px;
}

@ -0,0 +1,410 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Button, Col, Divider, Icon, message, notification, Popconfirm, Row, Spin, Tooltip, Typography} from "antd";
import "./Cluster.css";
import axios from "axios";
import {withConfigContext} from "../../../../../context/ConfigContext";
import AddAppsToClusterModal from "./AddAppsToClusterModal/AddAppsToClusterModal";
import {handleApiError} from "../../../../../js/Utils";
const {Title} = Typography;
class Cluster extends React.Component {
constructor(props) {
super(props);
const {cluster, pageId} = this.props;
this.originalCluster = Object.assign({}, cluster);
const {name, products, clusterId} = cluster;
this.clusterId = clusterId;
this.pageId = pageId;
this.state = {
name,
products,
isSaveable: false,
loading: false
};
}
handleNameChange = (name) => {
this.setState({
name
});
if (name !== this.originalCluster.name) {
this.setState({
isSaveable: true
});
}
};
isProductsChanged = (currentProducts) => {
let isChanged = false;
const originalProducts = this.originalCluster.products;
if (currentProducts.length === originalProducts.length) {
for (let i = 0; i < currentProducts.length; i++) {
if (currentProducts[i].packageId !== originalProducts[i].packageId) {
isChanged = true;
break;
}
}
} else {
isChanged = true;
}
return isChanged;
};
swapProduct = (index, swapIndex) => {
const products = [...this.state.products];
if (swapIndex !== -1 && index < products.length) {
// swap elements
[products[index], products[swapIndex]] = [products[swapIndex], products[index]];
this.setState({
products,
});
this.setState({
isSaveable: this.isProductsChanged(products)
})
}
};
removeProduct = (index) => {
const products = [...this.state.products];
products.splice(index, 1);
this.setState({
products,
isSaveable: true
});
};
getCurrentCluster = () => {
const {products, name} = this.state;
return {
pageId: this.pageId,
clusterId: this.clusterId,
name: name,
products: products,
orderInPage: this.props.orderInPage
};
};
resetChanges = () => {
const cluster = this.originalCluster;
const {name, products} = cluster;
this.setState({
loading: false,
name,
products,
isSaveable: false
});
};
updateCluster = () => {
const config = this.props.context;
const cluster = this.getCurrentCluster();
this.setState({loading: true});
axios.put(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/cluster",
cluster
).then(res => {
if (res.status === 200) {
notification["success"]({
message: 'Saved!',
description: 'Cluster updated successfully!'
});
const cluster = res.data.data;
const {name, products} = cluster;
this.originalCluster = Object.assign({}, cluster);
this.resetChanges();
if (this.props.toggleAddNewClusterVisibility !== undefined) {
this.props.toggleAddNewClusterVisibility(false);
}
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to update the cluster.");
this.setState({loading: false});
});
};
deleteCluster = () => {
const config = this.props.context;
this.setState({loading: true});
axios.delete(
window.location.origin + config.serverConfig.invoker.uri +
`/device-mgt/android/v1.0/enterprise/store-layout/cluster/${this.clusterId}/page/${this.pageId}`
).then(res => {
if (res.status === 200) {
notification["success"]({
message: 'Done!',
description: 'Cluster deleted successfully!'
});
this.setState({
loading: false,
});
this.props.removeLoadedCluster(this.clusterId);
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to update the cluster.");
this.setState({loading: false});
});
};
getUnselectedProducts = () => {
const {applications} = this.props;
const selectedProducts = this.state.products;
// get a copy from all products
const unSelectedProducts = [...applications];
// remove selected products from unselected products
selectedProducts.forEach((selectedProduct) => {
for (let i = 0; i < unSelectedProducts.length; i++) {
if (selectedProduct.packageId === unSelectedProducts[i].packageId) {
// remove item from array
unSelectedProducts.splice(i, 1);
}
}
});
return unSelectedProducts;
};
addSelectedProducts = (products) => {
this.setState({
products: [...this.state.products, ...products],
isSaveable: products.length > 0
});
};
cancelAddingNewCluster = () => {
this.resetChanges();
this.props.toggleAddNewClusterVisibility(false);
};
saveNewCluster = () => {
const config = this.props.context;
const cluster = this.getCurrentCluster();
this.setState({loading: true});
axios.post(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/cluster",
cluster
).then(res => {
if (res.status === 200) {
notification["success"]({
message: 'Saved!',
description: 'Cluster updated successfully!'
});
const cluster = res.data.data;
this.resetChanges();
this.props.addSavedClusterToThePage(cluster);
}
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin + '/publisher/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to update the cluster.",
});
}
this.setState({loading: false});
});
};
render() {
const {name, products, loading} = this.state;
const unselectedProducts = this.getUnselectedProducts();
const {isTemporary, index} = this.props;
const Product = ({product, index}) => {
const {packageId} = product;
let imageSrc = "";
const iconUrl = product.iconUrl;
// check if the icon url is an url or google image id
if (iconUrl.startsWith("http")) {
imageSrc = iconUrl;
} else {
imageSrc = `https://lh3.googleusercontent.com/${iconUrl}=s240-rw`;
}
return (
<div className="product">
<div className="arrow">
<button disabled={index === 0} className="btn"
onClick={() => {
this.swapProduct(index, index - 1);
}}>
<Icon type="caret-left" theme="filled"/>
</button>
</div>
<div className="product-icon">
<img src={imageSrc}/>
<Tooltip title={packageId}>
<div className="title">
{packageId}
</div>
</Tooltip>
</div>
<div className="arrow">
<button
disabled={index === products.length - 1}
onClick={() => {
this.swapProduct(index, index + 1);
}} className="btn btn-right"><Icon type="caret-right" theme="filled"/></button>
</div>
<div className="delete-btn">
<button className="btn"
onClick={() => {
this.removeProduct(index)
}}>
<Icon type="close-circle" theme="filled"/>
</button>
</div>
</div>
);
};
return (
<div className="cluster" id={this.props.orderInPage}>
<Spin spinning={loading}>
<Row>
<Col span={16}>
<Title editable={{onChange: this.handleNameChange}} level={4}>{name}</Title>
</Col>
<Col span={8}>
{!isTemporary && (
<div style={{float: "right"}}>
<Tooltip title="Move Up">
<Button
type="link"
icon="caret-up"
size="large"
onClick={() => {
this.props.swapClusters(index, index - 1)
}} htmlType="button"/>
</Tooltip>
<Tooltip title="Move Down">
<Button
type="link"
icon="caret-down"
size="large"
onClick={() => {
this.props.swapClusters(index, index + 1)
}} htmlType="button"/>
</Tooltip>
<Tooltip title="Delete Cluster">
<Popconfirm
title="Are you sure?"
okText="Yes"
cancelText="No"
onConfirm={this.deleteCluster}>
<Button
type="danger"
icon="delete"
shape="circle"
htmlType="button"/>
</Popconfirm>
</Tooltip>
</div>
)}
</Col>
</Row>
<div className="products-row">
<AddAppsToClusterModal
addSelectedProducts={this.addSelectedProducts}
unselectedProducts={unselectedProducts}/>
{
products.map((product, index) => {
return (
<Product
key={product.packageId}
product={product}
index={index}/>
);
})
}
</div>
<Row>
<Col>
{isTemporary && (
<div>
<Button
onClick={this.cancelAddingNewCluster}>
Cancel
</Button>
<Divider type="vertical"/>
<Tooltip
title={(products.length === 0) ? "You must add applications to the cluster before saving" : ""}>
<Button
disabled={products.length === 0}
onClick={this.saveNewCluster}
htmlType="button" type="primary">
Save
</Button>
</Tooltip>
</div>
)}
{!isTemporary && (
<div>
<Button
onClick={this.resetChanges}
disabled={!this.state.isSaveable}>
Cancel
</Button>
<Divider type="vertical"/>
<Button
onClick={this.updateCluster}
htmlType="button" type="primary"
disabled={!this.state.isSaveable}>
Save
</Button>
</div>
)}
</Col>
</Row>
</Spin>
</div>
);
}
}
export default withConfigContext(Cluster);

@ -0,0 +1,118 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Button, message, Modal, notification, Select, Spin} from "antd";
import axios from "axios";
import {withConfigContext} from "../../../../../context/ConfigContext";
import {handleApiError} from "../../../../../js/Utils";
const {Option} = Select;
class EditLinks extends React.Component {
constructor(props) {
super(props);
this.selectedLinks = [];
this.state = {
visible: false
};
}
showModal = () => {
this.setState({
visible: true,
loading: false
});
};
handleCancel = e => {
this.setState({
visible: false,
});
};
updateLinks = () => {
const config = this.props.context;
this.setState({loading: true});
axios.put(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/page-link",
{
pageId: this.props.pageId,
links: this.selectedLinks
}
).then(res => {
if (res.status === 200) {
notification["success"]({
message: 'Saved!',
description: 'Links updated successfully!'
});
this.props.updateLinks(this.selectedLinks);
this.setState({
loading: false,
visible: false
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to update the cluster.");
this.setState({loading: false});
});
};
handleChange= (selectedLinks) =>{
this.selectedLinks = selectedLinks;
};
render() {
return (
<div>
<Button onClick={this.showModal} type="link">[add / remove links]</Button>
<Modal
title="Add / Remove Links"
visible={this.state.visible}
onOk={this.updateLinks}
onCancel={this.handleCancel}
okText="Update">
<Spin spinning={this.state.loading}>
<Select
mode="multiple"
style={{width: '100%'}}
placeholder="Please select links"
defaultValue={this.props.selectedLinks}
onChange={this.handleChange}>
{
this.props.pages.map((page) => (
<Option disabled={page.id===this.props.pageId} key={page.id}>
{page.name[0]["text"]}
</Option>))
}
</Select>
</Spin>
</Modal>
</div>
);
}
}
export default withConfigContext(EditLinks);

@ -0,0 +1,33 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.layout-pages .action {
cursor: pointer;
}
.layout-pages .edit {
color: #008dff;
}
.layout-pages .btn-warning {
color: #faad14;
}
.layout-pages .btn-warning:hover {
color: #fa8905;
}

@ -0,0 +1,263 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import axios from "axios";
import {Tag, message, notification, Table, Typography, Divider, Icon, Popconfirm, Button} from "antd";
import {withConfigContext} from "../../../../context/ConfigContext";
import "./Pages.css";
import {Link} from "react-router-dom";
import AddNewPage from "../AddNewPage/AddNewPage";
import {handleApiError} from "../../../../js/Utils";
const {Text, Title} = Typography;
let config = null;
class Pages extends React.Component {
constructor(props) {
super(props);
config = this.props.context;
// TimeAgo.addLocale(en);
this.state = {
data: [],
pagination: {},
loading: false,
selectedRows: [],
homePageId: null
};
}
rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
// console.lohhhg(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
this.setState({
selectedRows: selectedRows
})
}
};
componentDidMount() {
this.setHomePage();
this.fetch();
}
//fetch data from api
fetch = (params = {}) => {
const config = this.props.context;
this.setState({loading: true});
// get current page
const currentPage = (params.hasOwnProperty("page")) ? params.page : 1;
const extraParams = {
offset: 10 * (currentPage - 1), //calculate the offset
limit: 10,
};
//send request to the invoker
axios.get(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/page",
).then(res => {
if (res.status === 200) {
const pagination = {...this.state.pagination};
this.setState({
loading: false,
data: res.data.data.page,
pagination,
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load pages.");
this.setState({loading: false});
});
};
setHomePage = () => {
const config = this.props.context;
//send request to the invoker
axios.get(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/home-page",
).then(res => {
if (res.status === 200) {
this.setState({
homePageId: res.data.data.homepageId
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to get home page.");
this.setState({loading: false});
});
};
updateHomePage = (pageId) => {
const config = this.props.context;
this.setState({
loading: true
});
//send request to the invoker
axios.put(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/home-page/" + pageId,
{}
).then(res => {
if (res.status === 200) {
notification["success"]({
message: "Done!",
description:
"Home page was updated successfully!",
});
this.setState({
homePageId: res.data.data.homepageId,
loading: false
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to update the home page.");
this.setState({loading: false});
});
};
deletePage = (pageId) => {
const {data} = this.state;
const config = this.props.context;
this.setState({
loading: true
});
//send request to the invoker
axios.delete(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/page/" + pageId
).then(res => {
if (res.status === 200) {
notification["success"]({
message: "Done!",
description:
"Home page was updated successfully!",
});
for( let i = 0; i < data.length; i++){
if ( data[i].id === pageId) {
data.splice(i, 1);
}
}
this.setState({
loading: false,
data: data
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to delete the page.");
this.setState({loading: false});
});
};
handleTableChange = (pagination, filters, sorter) => {
const pager = {...this.state.pagination};
pager.current = pagination.current;
this.setState({
pagination: pager,
});
};
columns = [
{
title: 'Page',
dataIndex: 'name',
key: 'name',
width: 300,
render: (name, page) => {
const pageName = name[0].text;
return (<div>
<Link to={`/publisher/manage/android-enterprise/pages/${pageName}/${page.id}`}> {pageName + " "}</Link>
{(page.id === this.state.homePageId) && (<Tag color="#badc58">Home Page</Tag>)}
</div>)
}
},
{
title: 'Actions',
key: 'actions',
render: (name, page) => (
<div>
<span className="action">
<Button disabled={page.id === this.state.homePageId}
className="btn-warning"
icon="home"
type="link"
onClick={() => {
this.updateHomePage(page.id);
}}>
set as homepage
</Button>
</span>
<Divider type="vertical"/>
<Popconfirm
title="Are you sure"
okText="Yes"
cancelText="No"
onConfirm={() => {
this.deletePage(page.id);
}}>
<span className="action">
<Text type="danger"><Icon type="delete"/> delete</Text>
</span>
</Popconfirm>
</div>
),
},
];
render() {
const {data, pagination, loading, selectedRows} = this.state;
return (
<div className="layout-pages">
<Title level={4}>Pages</Title>
<AddNewPage/>
<div style={{backgroundColor: "#ffffff", borderRadius: 5}}>
<Table
columns={this.columns}
rowKey={record => record.id}
dataSource={data}
pagination={{
...pagination,
size: "small",
// position: "top",
showTotal: (total, range) => `showing ${range[0]}-${range[1]} of ${total} pages`,
showQuickJumper: true
}}
loading={loading}
onChange={this.handleTableChange}
// rowSelection={this.rowSelection}
scroll={{x: 1000}}
/>
</div>
</div>
);
}
}
export default withConfigContext(Pages);

@ -0,0 +1,79 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Button, notification} from "antd";
import axios from "axios";
import {withConfigContext} from "../../../context/ConfigContext";
import {handleApiError} from "../../../js/Utils";
class SyncAndroidApps extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false
}
}
syncApps = () => {
const config = this.props.context;
this.setState({
loading: true
});
axios.get(
window.location.origin + config.serverConfig.invoker.uri + "/device-mgt/android/v1.0/enterprise/products/sync",
).then(res => {
notification["success"]({
message: "Done!",
description:
"Apps synced successfully!",
});
this.setState({
loading: false
});
}).catch((error) => {
handleApiError(error, "Error occurred while syncing the apps.");
this.setState({
loading: false
})
});
};
render() {
const {loading} = this.state;
return (
<div style={{display: "inline-block", padding: 4}}>
<Button
onClick={this.syncApps}
loading={loading}
style={{marginTop: 16}}
type="primary"
icon="sync"
>
Sync{loading && "ing..."}
</Button>
</div>
)
}
}
export default withConfigContext(SyncAndroidApps);

@ -87,12 +87,7 @@ class AddNewAppFormComponent extends React.Component {
axios.post(
url,
data,
{
headers: {
'X-Platform': config.serverConfig.platform
},
}
data
).then(res => {
if (res.status === 201) {
this.setState({

@ -95,7 +95,7 @@ class NewAppUploadForm extends React.Component {
};
if (formConfig.installationType !== "WEB_CLIP" && formConfig.installationType !== "CUSTOM") {
release.supportedOsVersions = "4.0-10.0";
release.supportedOsVersions = "4-30";
}
if (specificElements.hasOwnProperty("version")) {

@ -88,7 +88,7 @@ class AddNewReleaseFormComponent extends React.Component {
isSharedWithAllTenants,
metaData: "string",
releaseType: releaseType,
supportedOsVersions: "4.0-10.0"
supportedOsVersions: "4-30"
};
data.append('binaryFile', binaryFile[0].originFileObj);
@ -108,12 +108,7 @@ class AddNewReleaseFormComponent extends React.Component {
axios.post(
url,
data,
{
headers: {
'X-Platform': config.serverConfig.platform
},
}
data
).then(res => {
if (res.status === 201) {
this.setState({

@ -21,6 +21,7 @@
<head>
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"/>
<title>Entgra App Publisher</title>
<script src="https://apis.google.com/js/client.js"></script>
</head>
<div id="root"></div>
</html>

@ -31,6 +31,8 @@ import AddNewPublicApp from "./pages/dashboard/add-new-app/AddNewPublicApp";
import AddNewWebClip from "./pages/dashboard/add-new-app/AddNewWebClip";
import AddNewRelease from "./pages/dashboard/add-new-release/AddNewRelease";
import AddNewCustomApp from "./pages/dashboard/add-new-app/AddNewCustomApp";
import ManageAndroidEnterprise from "./pages/dashboard/manage/android-enterprise/ManageAndroidEnterprise";
import Page from "./pages/dashboard/manage/android-enterprise/page/Page";
const routes = [
@ -83,6 +85,16 @@ const routes = [
path: '/publisher/manage',
component: Mange,
exact: true
},
{
path: '/publisher/manage/android-enterprise',
component: ManageAndroidEnterprise,
exact: true
},
{
path: '/publisher/manage/android-enterprise/pages/:pageName/:pageId',
component: Page,
exact: true
}
]
}

@ -0,0 +1,32 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {message, notification} from "antd";
export const handleApiError = (error, message) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin + '/publisher/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description: message,
});
}
};

@ -77,18 +77,22 @@ class Dashboard extends React.Component {
<Icon type="plus"/>
Add New App
</span>
}>
<Menu.Item key="setting:1">
<Link to="/publisher/add-new-app/public">Public APP</Link>
</Menu.Item>
<Menu.Item key="setting:2">
<Link to="/publisher/add-new-app/enterprise">Enterprise APP</Link>
}
>
<Menu.Item key="add-new-public-app">
<Link to="/publisher/add-new-app/public">
Public APP
</Link>
</Menu.Item>
<Menu.Item key="setting:3">
<Link to="/publisher/add-new-app/web-clip">Web Clip</Link>
<Menu.Item key="add-new-enterprise-app">
<Link to="/publisher/add-new-app/enterprise">
Enterprise APP
</Link>
</Menu.Item>
<Menu.Item key="setting:4">
<Link to="/publisher/add-new-app/custom-app">Custom App</Link>
<Menu.Item key="add-new-web-clip">
<Link to="/publisher/add-new-app/web-clip">
Web Clip
</Link>
</Menu.Item>
</SubMenu>

@ -24,22 +24,6 @@ import {Link} from "react-router-dom";
const {Paragraph} = Typography;
const routes = [
{
path: 'index',
breadcrumbName: 'Publisher',
},
{
path: 'first',
breadcrumbName: 'Dashboard',
},
{
path: 'second',
breadcrumbName: 'Manage',
},
];
class Manage extends React.Component {
routes;
@ -57,10 +41,13 @@ class Manage extends React.Component {
<Breadcrumb.Item>
<Link to="/publisher/apps"><Icon type="home"/> Home</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Manage</Breadcrumb.Item>
<Breadcrumb.Item>
Manage
</Breadcrumb.Item>
<Breadcrumb.Item>General</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Manage</h3>
<h3>Manage General Settings</h3>
<Paragraph>Maintain and manage categories and tags here..</Paragraph>
</div>
</PageHeader>

@ -0,0 +1,68 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {PageHeader, Typography, Breadcrumb, Divider, Button, Icon} from "antd";
import {Link} from "react-router-dom";
import SyncAndroidApps from "../../../../components/manage/android-enterprise/SyncAndroidApps";
import {withConfigContext} from "../../../../context/ConfigContext";
import GooglePlayIframe from "../../../../components/manage/android-enterprise/GooglePlayIframe";
import Pages from "../../../../components/manage/android-enterprise/Pages/Pages";
const {Paragraph} = Typography;
class ManageAndroidEnterprise extends React.Component {
routes;
constructor(props) {
super(props);
this.routes = props.routes;
this.config = this.props.context;
}
render() {
return (
<div>
<PageHeader style={{paddingTop: 0}}>
<Breadcrumb style={{paddingBottom: 16}}>
<Breadcrumb.Item>
<Link to="/publisher/apps"><Icon type="home"/> Home</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>
Manage
</Breadcrumb.Item>
<Breadcrumb.Item>Android Enterprise</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Manage Android Enterprise</h3>
{/*<Paragraph>Lorem ipsum</Paragraph>*/}
</div>
</PageHeader>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 720}}>
<SyncAndroidApps/>
<GooglePlayIframe/>
<Divider/>
<Pages/>
</div>
</div>
);
}
}
export default withConfigContext(ManageAndroidEnterprise);

@ -0,0 +1,396 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {
PageHeader,
Typography,
Breadcrumb,
Button,
Icon,
Col,
Row,
notification,
message,
Spin,
Select,
Tag,
Divider
} from "antd";
import {Link, withRouter} from "react-router-dom";
import {withConfigContext} from "../../../../../context/ConfigContext";
import axios from "axios";
import Cluster from "../../../../../components/manage/android-enterprise/Pages/Cluster/Cluster";
import EditLinks from "../../../../../components/manage/android-enterprise/Pages/EditLinks/EditLinks";
const {Option} = Select;
const {Title, Text} = Typography;
class Page extends React.Component {
routes;
constructor(props) {
super(props);
const {pageName, pageId} = this.props.match.params;
this.pageId = pageId;
this.routes = props.routes;
this.config = this.props.context;
this.pages = [];
this.pageNames = {};
this.state = {
pageName,
clusters: [],
loading: false,
applications: [],
isAddNewClusterVisible: false,
links: []
};
}
componentDidMount() {
this.fetchClusters();
this.fetchApplications();
this.fetchPages();
}
removeLoadedCluster = (clusterId) => {
const clusters = [...this.state.clusters];
let index = -1;
for (let i = 0; i < clusters.length; i++) {
if (clusters[i].clusterId === clusterId) {
index = i;
break;
}
}
clusters.splice(index, 1);
this.setState({
clusters
});
};
updatePageName = pageName => {
const config = this.props.context;
if (pageName !== this.state.pageName && pageName !== "") {
const data = {
locale: "en",
pageName: pageName,
pageId: this.pageId
};
axios.put(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/page",
data
).then(res => {
if (res.status === 200) {
notification["success"]({
message: 'Saved!',
description: 'Page name updated successfully!'
});
this.setState({
loading: false,
pageName: res.data.data.pageName,
});
this.props.history.push(`/publisher/manage/android-enterprise/pages/${pageName}/${this.pageId}`);
}
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin + '/publisher/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to save the page name.",
});
}
this.setState({loading: false});
});
}
};
swapClusters = (index, swapIndex) => {
const clusters = [...this.state.clusters];
if (swapIndex !== -1 && index < clusters.length) {
// swap elements
[clusters[index], clusters[swapIndex]] = [clusters[swapIndex], clusters[index]];
this.setState({
clusters,
});
}
};
fetchPages = () => {
const config = this.props.context;
this.setState({loading: true});
//send request to the invoker
axios.get(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/page",
).then(res => {
if (res.status === 200) {
this.pages = res.data.data.page;
let links = [];
this.pages.forEach((page) => {
this.pageNames[page.id.toString()] = page.name[0]["text"];
if (page.id === this.pageId && page.hasOwnProperty("link")) {
links = page["link"];
}
});
this.setState({
loading: false,
links
});
}
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin + '/publisher/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load pages.",
});
}
this.setState({loading: false});
});
};
fetchClusters = () => {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri +
`/device-mgt/android/v1.0/enterprise/store-layout/page/${this.pageId}/clusters`
).then(res => {
if (res.status === 200) {
let clusters = JSON.parse(res.data.data);
// sort according to the orderInPage value
clusters.sort((a, b) => (a.orderInPage > b.orderInPage) ? 1 : -1);
this.setState({
clusters,
loading: false
});
}
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
window.location.href = window.location.origin + '/publisher/login';
} else if (!(error.hasOwnProperty("response") && error.response.status === 404)) {
// API sends 404 when no apps
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load clusters.",
});
}
this.setState({
loading: false
});
});
};
//fetch applications
fetchApplications = () => {
const config = this.props.context;
this.setState({loading: true});
const filters = {
appType: "PUBLIC",
deviceType: "android"
};
//send request to the invoker
axios.post(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications",
filters
).then(res => {
if (res.status === 200) {
const applications = res.data.data.applications.map(application => {
const release = application.applicationReleases[0];
return {
packageId: `app:${application.packageName}`,
iconUrl: release.iconPath,
name: application.name
}
});
this.setState({
loading: false,
applications,
});
}
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin + '/publisher/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load pages.",
});
}
this.setState({loading: false});
});
};
toggleAddNewClusterVisibility = (isAddNewClusterVisible) => {
this.setState({
isAddNewClusterVisible
});
};
addSavedClusterToThePage = (cluster) => {
this.setState({
clusters: [...this.state.clusters, cluster],
isAddNewClusterVisible: false
});
window.scrollTo(0, document.body.scrollHeight);
};
updateLinks = (links) =>{
this.setState({
links
});
};
render() {
const {pageName, loading, clusters, applications, isAddNewClusterVisible, links} = this.state;
return (
<div>
<PageHeader style={{paddingTop: 0}}>
<Breadcrumb style={{paddingBottom: 16}}>
<Breadcrumb.Item>
<Link to="/publisher/apps"><Icon type="home"/> Home</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>
Manage
</Breadcrumb.Item>
<Breadcrumb.Item>
<Link to="/publisher/manage/android-enterprise">Android Enterprise</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Manage Page</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Manage Android Enterprise</h3>
{/*<Paragraph>Lorem ipsum</Paragraph>*/}
</div>
</PageHeader>
<Spin spinning={loading}>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 720}}>
<Row>
<Col md={8} sm={18} xs={24}>
<Title editable={{onChange: this.updatePageName}} level={2}>{pageName}</Title>
</Col>
</Row>
<Row>
<Col>
<Title level={4}>Links</Title>
{
links.map(link => {
if (this.pageNames.hasOwnProperty(link.toString())) {
return <Tag key={link}
color="#87d068">{this.pageNames[link.toString()]}</Tag>
} else {
return null;
}
})
}
<EditLinks
updateLinks={this.updateLinks}
pageId={this.pageId}
selectedLinks={links}
pages={this.pages}/>
</Col>
{/*<Col>*/}
{/*</Col>*/}
</Row>
<Divider dashed={true}/>
<Title level={4}>Clusters</Title>
<div hidden={isAddNewClusterVisible} style={{textAlign: "center"}}>
<Button
type="dashed"
shape="round"
icon="plus"
size="large"
onClick={() => {
this.toggleAddNewClusterVisibility(true);
}}
>Add new cluster</Button>
</div>
<div hidden={!isAddNewClusterVisible}>
<Cluster
cluster={{
clusterId: 0,
name: "New Cluster",
products: []
}}
orderInPage={clusters.length}
isTemporary={true}
pageId={this.pageId}
applications={applications}
addSavedClusterToThePage={this.addSavedClusterToThePage}
toggleAddNewClusterVisibility={this.toggleAddNewClusterVisibility}/>
</div>
{
clusters.map((cluster, index) => {
return (
<Cluster
key={cluster.clusterId}
index={index}
orderInPage={cluster.orderInPage}
isTemporary={false}
cluster={cluster}
pageId={this.pageId}
applications={applications}
swapClusters={this.swapClusters}
removeLoadedCluster={this.removeLoadedCluster}/>
);
})
}
</div>
</Spin>
</div>
);
}
}
export default withConfigContext(withRouter(Page));

@ -78,7 +78,7 @@ const config = {
loader: "style-loader"
},
{
loader: "css-loader",
loader: "css-loader"
},
{
loader: "less-loader",

@ -0,0 +1,132 @@
<%
var log = new Log("api/enterprise.jag");
var uri = request.getRequestURI();
var uriMatcher = new URIMatcher(String(uri));
var constants = require("/app/modules/constants.js");
var devicemgtProps = require("/app/modules/conf-reader/main.js")["conf"];
var userModule = require("/app/modules/business-controllers/user.js")["userModule"];
var restAPIRequestDetails = request.getContent();
var result;
var user = session.get(constants.USER_SESSION_KEY);
// This checks if the session is valid
getAccessToken = function() {
if (session) {
var tokenPair = session.get(constants["TOKEN_PAIR"]);
if (tokenPair) {
return parse(tokenPair)["accessToken"];
}
}
return null;
};
callBackend = function(url, token, method, payload) {
var xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.open(method, url);
xmlHttpRequest.setRequestHeader(constants["AUTHORIZATION_HEADER"], constants["BEARER_PREFIX"] + token);
xmlHttpRequest.setRequestHeader(constants["CONTENT_TYPE_IDENTIFIER"], constants["APPLICATION_JSON"]);
xmlHttpRequest.setRequestHeader(constants["ACCEPT_IDENTIFIER"], constants["APPLICATION_JSON"]);
if (payload) {
xmlHttpRequest.send(payload);
} else {
xmlHttpRequest.send();
}
response["status"] = xmlHttpRequest["status"];
if (xmlHttpRequest["responseText"]) {
result = xmlHttpRequest["responseText"];
response["content"] = xmlHttpRequest["responseText"];
}
};
var accessToken = getAccessToken();
if (!user || accessToken == null) {
response.sendRedirect("/devicemgt/login?#login-required");
exit();
} else {
response.contentType = 'application/json';
if (uriMatcher.match("/{context}/api/enterprise/token")) {
session.put("externalEndpoint", restAPIRequestDetails["endpoint"]);
session.put("externalToken", restAPIRequestDetails["externalToken"]);
log.info("Calling get token");
callBackend(restAPIRequestDetails["endpoint"], session.get("externalToken"), "POST", restAPIRequestDetails);
if (response["status"] && response["status"] == 200) {
var completionToken = parse(result)["completionToken"];
if (completionToken) {
log.info("Token received");
session.put("completionToken", completionToken)
}
}
} else if (uriMatcher.match("/{context}/api/enterprise/enroll-complete")) {
var tokenEndpoint = session.get("externalEndpoint")
var enterpriseEndpoint = tokenEndpoint.replace("signup-url", "complete-signup");
var completionToken = session.get("completionToken");
var enterpriseToken = request.getParameter("enterpriseToken");
log.debug("completionToken" + completionToken + ", enterpriseEndpoint" + enterpriseEndpoint +
", enterpriseToken" + enterpriseToken);
var requestPayload = {}
requestPayload.completionToken = completionToken;
requestPayload.enterpriseToken = enterpriseToken;
log.info("Calling complete-signup");
callBackend(enterpriseEndpoint, session.get("externalToken"), "POST", requestPayload);
var enterpriseId = parse(result)["id"];
if (enterpriseId) {
log.info("Calling complete-signup success");
var serviceAccountRequest = {};
serviceAccountRequest.enterpriseId = enterpriseId;
serviceAccountRequest.keyType = "googleCredentials"
var enterpriseEndpoint = tokenEndpoint.replace("signup-url", "create-esa");
log.info("Calling create-esa");
callBackend(enterpriseEndpoint, session.get("externalToken"), "POST", serviceAccountRequest);
var data = parse(result)["data"];
log.info("Calling create-esa success" + data);
var androidConfigAPI = devicemgtProps["httpsURL"] + "/api/device-mgt/android/v1.0/configuration";
log.info("fetching platform configs");
callBackend(androidConfigAPI, accessToken, "GET");
var configurationsList = parse(result);
for (var x = 0; x < configurationsList.configuration.length; x++) {
if (configurationsList.configuration[x].name == "esa" || configurationsList.configuration[x].name == "enterpriseId") {
configurationsList.configuration.splice(x, 1);
}
}
log.info("fetching platform configs success");
var payloadToAdd = {};
payloadToAdd.contentType = "text";
payloadToAdd.name = "esa";
payloadToAdd.value = data;
var enterpriseIdPayload = {};
enterpriseIdPayload.contentType = "text";
enterpriseIdPayload.name = "enterpriseId";
enterpriseIdPayload.value = enterpriseId;
configurationsList.configuration[configurationsList.configuration.length] = payloadToAdd;
configurationsList.configuration[configurationsList.configuration.length] = enterpriseIdPayload;
log.info("saving platform configs");
callBackend(androidConfigAPI, accessToken, "PUT", configurationsList);
log.info("saving platform configs success");
if (response["status"] == 200) {
log.info("Process successful!! Redirecting...");
response.sendRedirect("/devicemgt/platform-configuration?enterprise-success=true");
}
}
} else if (uriMatcher.match("/{context}/api/enterprise/asdsad/unenroll")) {
session.put("externalEndpoint", restAPIRequestDetails["endpoint"]);
session.put("externalToken", restAPIRequestDetails["externalToken"]);
callBackend(restAPIRequestDetails["endpoint"], session.get("externalToken"), "POST", restAPIRequestDetails);
}
}
%>

@ -224,6 +224,7 @@ policyModule = function () {
appObjectFromRestEndpoint = appListFromRestEndpoint[i];
appObjectToView = {};
appObjectToView["appName"] = appObjectFromRestEndpoint["name"];
appObjectToView["packageName"] = appObjectFromRestEndpoint["packageName"];
appObjectToView["appId"] = appObjectFromRestEndpoint["id"];
if ("WEB_CLIP" === appObjectFromRestEndpoint["type"]) {
appObjectToView["type"] = "Web Clip"

@ -3,6 +3,10 @@
"logLevel": "info",
"initScripts": ["/app/modules/init.js"],
"urlMappings": [
{
"url": "/api/enterprise/*",
"path": "/api/enterprise.jag"
},
{
"url": "/api/devices/*",
"path": "/api/device-api.jag"

@ -271,7 +271,7 @@ public class InvokerHandler extends HttpServlet {
*/
private boolean validateRequest(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
serverUrl = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort();
serverUrl = req.getScheme() + "://" + req.getServerName() + ":" + System.getProperty("iot.gateway.https.port");
apiEndpoint = req.getPathInfo();
HttpSession session = req.getSession(false);

@ -225,8 +225,9 @@ public class LoginHandler extends HttpServlet {
password = req.getParameter("password");
platform = req.getParameter(HandlerConstants.PLATFORM);
serverUrl = req.getScheme() + HandlerConstants.SCHEME_SEPARATOR + req.getServerName() + HandlerConstants.COLON
+ req.getServerPort();
uiConfigUrl = serverUrl + HandlerConstants.UI_CONFIG_ENDPOINT;
+ System.getProperty("iot.gateway.https.port");
uiConfigUrl = req.getScheme() + HandlerConstants.SCHEME_SEPARATOR + req.getServerName() + HandlerConstants.COLON
+ System.getProperty("iot.gateway.carbon.https.port") + HandlerConstants.UI_CONFIG_ENDPOINT;
try {
if (platform == null) {

@ -22,7 +22,7 @@ public class HandlerConstants {
public static final String PUBLISHER_APPLICATION_NAME = "application-mgt-publisher";
public static final String APP_REG_ENDPOINT = "/api-application-registration/register";
public static final String UI_CONFIG_ENDPOINT = "/api/application-mgt/v1.0/config/ui-config";
public static final String TOKEN_ENDPOINT = "/oauth2/token";
public static final String TOKEN_ENDPOINT = "/token";
public static final String LOGIN_PAGE = "/login";
public static final String BASIC = "Basic ";
public static final String BEARER = "Bearer ";

@ -156,6 +156,12 @@
<Scope>perm:admin:app:review:view</Scope>
<Scope>perm:admin:app:publisher:update</Scope>
<Scope>perm:admin:app:review:update</Scope>
<Scope>perm:device-types:types</Scope>
<Scope>perm:enterprise:modify</Scope>
<Scope>perm:enterprise:view</Scope>
<Scope>perm:android-work:customer</Scope>
<Scope>perm:android-work:admin</Scope>
<Scope>perm:application-command:modify</Scope>
</Scopes>
<SSOConfiguration>
<Issuer>app-mgt</Issuer>
@ -176,6 +182,39 @@
<AppCategories>
<Category>EMM</Category>
<Category>IoT</Category>
<Category>Art &amp; Design</Category>
<Category>Auto &amp; Vehicles</Category>
<Category>Beauty</Category>
<Category>Books &amp; Reference</Category>
<Category>Business</Category>
<Category>Comics</Category>
<Category>Communications</Category>
<Category>Dating</Category>
<Category>Education</Category>
<Category>Entertainment</Category>
<Category>Events</Category>
<Category>Finance</Category>
<Category>Food &amp; Drink</Category>
<Category>Health &amp; Fitness</Category>
<Category>House &amp; Home</Category>
<Category>Libraries &amp; Demo</Category>
<Category>Lifestyle</Category>
<Category>Maps &amp; Navigation</Category>
<Category>Medical</Category>
<Category>Music &amp; Audio</Category>
<Category>News &amp; Magazines</Category>
<Category>Parenting</Category>
<Category>Personalization</Category>
<Category>Photography</Category>
<Category>Productivity</Category>
<Category>Shopping</Category>
<Category>Social</Category>
<Category>Sports</Category>
<Category>Tools</Category>
<Category>Travel &amp; Local</Category>
<Category>Video Players &amp; Editors</Category>
<Category>Weather</Category>
<Category>GooglePlaySyncedApp</Category>
</AppCategories>
<RatingConfig>

@ -21,7 +21,7 @@ PRIMARY KEY (ID)
CREATE TABLE IF NOT EXISTS AP_APP_RELEASE(
ID INTEGER NOT NULL AUTO_INCREMENT,
DESCRIPTION CLOB NOT NULL,
VERSION VARCHAR(20) NOT NULL,
VERSION VARCHAR(70) NOT NULL,
TENANT_ID INTEGER NOT NULL,
UUID VARCHAR(200) NOT NULL,
RELEASE_TYPE VARCHAR(45) NOT NULL,

@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS `APP_MANAGER`.`AP_APP_RELEASE`
(
`ID` INT(11) NOT NULL,
`DESCRIPTION` TEXT NOT NULL,
`VERSION` VARCHAR(20) NOT NULL,
`VERSION` VARCHAR(70) NOT NULL,
`TENANT_ID` INT(11) NOT NULL,
`UUID` VARCHAR(200) NOT NULL,
`RELEASE_TYPE` VARCHAR(45) NOT NULL,

@ -298,7 +298,7 @@ CREATE INDEX FK_APP_PROPERTY_APP ON APPM_APPLICATION_PROPERTY(APPLICATION_ID ASC
-- -----------------------------------------------------
CREATE TABLE APPM_APPLICATION_RELEASE (
ID INT UNIQUE ,
VERSION_NAME VARCHAR(100) NOT NULL,
VERSION_NAME VARCHAR(70) NOT NULL,
RELEASE_RESOURCE VARCHAR(2048) NULL,
RELEASE_CHANNEL VARCHAR(50) DEFAULT 'ALPHA',
RELEASE_DETAILS VARCHAR(2048) NULL,

@ -221,7 +221,7 @@ CREATE SEQUENCE APPM_APPLICATION_RELEASE_PK_SEQ;
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS APPM_APPLICATION_RELEASE (
ID INT DEFAULT NEXTVAL('APPM_APPLICATION_RELEASE_PK_SEQ') UNIQUE,
VERSION_NAME VARCHAR(100) NOT NULL,
VERSION_NAME VARCHAR(70) NOT NULL,
RELEASE_RESOURCE TEXT NULL,
RELEASE_CHANNEL VARCHAR(50) DEFAULT 'ALPHA',
RELEASE_DETAILS TEXT NULL,

Loading…
Cancel
Save