Merge branch 'application-mgt-new' into 'application-mgt-new'

Improve APPM error messages

See merge request entgra/carbon-device-mgt!208
feature/appm-store/pbac
Dharmakeerthi Lasantha 5 years ago
commit 16cefc8a80

4
.gitignore vendored

@ -35,4 +35,8 @@ components/application-mgt/org.wso2.carbon.device.application.mgt.store.ui/react
components/application-mgt/org.wso2.carbon.device.application.mgt.store.ui/react-app/dist/
components/application-mgt/org.wso2.carbon.device.application.mgt.store.ui/react-app/package-lock.json
components/application-mgt/org.wso2.carbon.device.application.mgt.store.ui/react-app/tmp/
components/device-mgt/io.entgra.device.mgt.ui/react-app/node_modules/
components/device-mgt/io.entgra.device.mgt.ui/react-app/dist/
components/device-mgt/io.entgra.device.mgt.ui/react-app/package-lock.json
components/device-mgt/io.entgra.device.mgt.ui/react-app/tmp/

@ -55,7 +55,7 @@ public class AuthenticationHandler extends AbstractHandler {
private static final String X_JWT_ASSERTION = "X-JWT-Assertion";
private static final String JWTTOKEN = "JWTToken";
private static final String AUTHORIZATION = "Authorization";
private static final String BEARER = "Bearer ";
private static final String BEARER = "Basic ";
private static final String CONTENT_TYPE = "Content-Type";
private IOTServerConfiguration iotServerConfiguration;
@ -95,7 +95,7 @@ public class AuthenticationHandler extends AbstractHandler {
log.debug("Verify Cert:\n" + mdmSignature);
}
URI certVerifyUrl = new URI(iotServerConfiguration.getVerificationEndpoint() + "ios");
Map<String, String> certVerifyHeaders = this.setHeaders(this.restInvoker);
Map<String, String> certVerifyHeaders = this.setHeaders();
Certificate certificate = new Certificate();
certificate.setPem(mdmSignature);
@ -127,7 +127,7 @@ public class AuthenticationHandler extends AbstractHandler {
String deviceType = this.getDeviceType(messageContext.getTo().getAddress().trim());
URI certVerifyUrl = new URI(iotServerConfiguration.getVerificationEndpoint() + deviceType);
Map<String, String> certVerifyHeaders = this.setHeaders(this.restInvoker);
Map<String, String> certVerifyHeaders = this.setHeaders();
Certificate certificate = new Certificate();
certificate.setPem(subjectDN);
certificate.setTenantId(tenantId);
@ -157,7 +157,7 @@ public class AuthenticationHandler extends AbstractHandler {
}
String deviceType = this.getDeviceType(messageContext.getTo().getAddress().trim());
URI certVerifyUrl = new URI(iotServerConfiguration.getVerificationEndpoint() + deviceType);
Map<String, String> certVerifyHeaders = this.setHeaders(this.restInvoker);
Map<String, String> certVerifyHeaders = this.setHeaders();
Certificate certificate = new Certificate();
certificate.setPem(encodedPem);
@ -184,9 +184,6 @@ public class AuthenticationHandler extends AbstractHandler {
} catch (URISyntaxException e) {
log.error("Error while processing certificate.", e);
return false;
} catch (APIMCertificateMGTException e) {
log.error("Error while processing certificate.", e);
return false;
} catch (CertificateException e) {
log.error("Certificate issue occurred when generating converting PEM to x509Certificate", e);
return false;
@ -212,9 +209,9 @@ public class AuthenticationHandler extends AbstractHandler {
return null;
}
private Map<String, String> setHeaders(RESTInvoker restInvoker) throws APIMCertificateMGTException {
private Map<String, String> setHeaders() {
Map<String, String> map = new HashMap<>();
String accessToken = Utils.getAccessToken(iotServerConfiguration, restInvoker);
String accessToken = Utils.getBase64EncodedToken(iotServerConfiguration);
map.put(AUTHORIZATION, BEARER + accessToken);
map.put(CONTENT_TYPE, "application/json");
return map;

@ -135,38 +135,14 @@ public class Utils {
}
/**
* This class get the access token from the key manager.
* This method is used to get the base64 encoded token.
*
* @param iotServerConfiguration Instance of the IoTsererConfiguration.
* @return Access token will be returned.
* @throws APIMCertificateMGTException
*/
public static String getAccessToken(IOTServerConfiguration iotServerConfiguration, RESTInvoker restInvoker)
throws APIMCertificateMGTException {
try {
if (clientId == null || clientSecret == null) {
getClientSecretes(iotServerConfiguration, restInvoker);
}
URI tokenUrl = new URI(iotServerConfiguration.getOauthTokenEndpoint());
String tokenContent = "grant_type=password&username=" + iotServerConfiguration.getUsername() + "&password=" +
iotServerConfiguration.getPassword() + "&scope=activity-view";
String tokenBasicAuth = "Basic " + Base64.encode((clientId + ":" + clientSecret).getBytes());
Map<String, String> tokenHeaders = new HashMap<>();
tokenHeaders.put("Authorization", tokenBasicAuth);
tokenHeaders.put("Content-Type", "application/x-www-form-urlencoded");
RESTResponse response = restInvoker.invokePOST(tokenUrl, tokenHeaders, tokenContent);
if (log.isDebugEnabled()) {
log.debug("Token response:" + response.getContent());
}
JSONObject jsonResponse = new JSONObject(response.getContent());
return jsonResponse.getString("access_token");
} catch (URISyntaxException | IOException e) {
throw new APIMCertificateMGTException("Error occurred while trying to call oauth token endpoint", e);
} catch (JSONException e) {
throw new APIMCertificateMGTException("Error occurred while converting the json to object", e);
}
public static String getBase64EncodedToken(IOTServerConfiguration iotServerConfiguration) {
return Base64.encode((iotServerConfiguration.getUsername() + ":" + iotServerConfiguration.getPassword()).
getBytes());
}
/**

@ -92,8 +92,6 @@ public class AuthenticationHandlerTest extends BaseAPIHandlerTest {
HashMap<String, String> transportHeaders = new HashMap<>();
transportHeaders.put(AuthConstants.MDM_SIGNATURE, "some cert");
setMockClient();
this.mockClient.setResponse(getDCRResponse());
this.mockClient.setResponse(getAccessTokenReponse());
this.mockClient.setResponse(getValidationResponse());
boolean response = this.handler.handleRequest(createSynapseMessageContext("<empty/>", this.synapseConfiguration,
transportHeaders, "https://test.com/testservice/device-mgt/testdevice"));
@ -107,7 +105,6 @@ public class AuthenticationHandlerTest extends BaseAPIHandlerTest {
HashMap<String, String> transportHeaders = new HashMap<>();
transportHeaders.put(AuthConstants.PROXY_MUTUAL_AUTH_HEADER, "Test Header");
setMockClient();
this.mockClient.setResponse(getAccessTokenReponse());
this.mockClient.setResponse(getValidationResponse());
boolean response = this.handler.handleRequest(createSynapseMessageContext("<empty/>", this.synapseConfiguration,
transportHeaders, "https://test.com/testservice/device-mgt/testdevice"));
@ -121,7 +118,6 @@ public class AuthenticationHandlerTest extends BaseAPIHandlerTest {
HashMap<String, String> transportHeaders = new HashMap<>();
transportHeaders.put(AuthConstants.MUTUAL_AUTH_HEADER, "Test Header");
setMockClient();
this.mockClient.setResponse(getAccessTokenReponse());
this.mockClient.setResponse(getValidationResponse());
MessageContext messageContext = createSynapseMessageContext("<empty/>", this.synapseConfiguration,
transportHeaders, "https://test.com/testservice/device-mgt/testdevice");
@ -141,7 +137,6 @@ public class AuthenticationHandlerTest extends BaseAPIHandlerTest {
HashMap<String, String> transportHeaders = new HashMap<>();
transportHeaders.put(AuthConstants.ENCODED_PEM, "encoded pem");
setMockClient();
this.mockClient.setResponse(getAccessTokenReponse());
this.mockClient.setResponse(getValidationResponse());
MessageContext messageContext = createSynapseMessageContext("<empty/>", this.synapseConfiguration,
transportHeaders, "https://test.com/testservice/device-mgt/testdevice");
@ -156,7 +151,6 @@ public class AuthenticationHandlerTest extends BaseAPIHandlerTest {
HashMap<String, String> transportHeaders = new HashMap<>();
transportHeaders.put(AuthConstants.ENCODED_PEM, "encoded pem");
setMockClient();
this.mockClient.setResponse(getAccessTokenReponse());
this.mockClient.setResponse(getInvalidResponse());
MessageContext messageContext = createSynapseMessageContext("<empty/>", this.synapseConfiguration,
transportHeaders, "https://test.com/testservice/device-mgt/testdevice");
@ -185,7 +179,6 @@ public class AuthenticationHandlerTest extends BaseAPIHandlerTest {
HashMap<String, String> transportHeaders = new HashMap<>();
transportHeaders.put(AuthConstants.ENCODED_PEM, "encoded pem");
setMockClient();
this.mockClient.setResponse(getAccessTokenReponse());
this.mockClient.setResponse(null);
MessageContext messageContext = createSynapseMessageContext("<empty/>", this.synapseConfiguration,
transportHeaders, "https://test.com/testservice/device-mgt/testdevice");

@ -147,9 +147,8 @@ public class GenericSubscriptionDAOImpl extends AbstractDAOImpl implements Subsc
log.error(msg);
throw new ApplicationManagementDAOException(msg, e);
} catch (SQLException e) {
String msg = "Error occurred when obtaining database connection for updating the device subscriptions of "
+ "application. Updated by: " + updateBy + " and updating action triggered from "
+ actionTriggeredFrom;
String msg = "Error occurred while executing SQL to update the device subscriptions of application. "
+ "Updated by: " + updateBy + " and updating action triggered from " + actionTriggeredFrom;
log.error(msg);
throw new ApplicationManagementDAOException(msg, e);
}
@ -356,7 +355,7 @@ public class GenericSubscriptionDAOImpl extends AbstractDAOImpl implements Subsc
log.error(msg);
throw new ApplicationManagementDAOException(msg, e);
} catch (SQLException e) {
String msg = "Error occurred while getting device subscription data for application ID: " + appReleaseId;
String msg = "Error occurred while while running SQL to get device subscription data for application ID: " + appReleaseId;
log.error(msg);
throw new ApplicationManagementDAOException(msg, e);
}
@ -618,12 +617,12 @@ public class GenericSubscriptionDAOImpl extends AbstractDAOImpl implements Subsc
stmt.executeBatch();
}
} catch (DBConnectionException e) {
String msg = "Error occurred while obtaining the DB connection to update the user subscriptions of "
+ "application.";
String msg = "Error occurred while obtaining the DB connection to update the user/role/group subscriptions "
+ "of application.";
log.error(msg);
throw new ApplicationManagementDAOException(msg, e);
} catch (SQLException e) {
String msg = "Error occurred when obtaining database connection for updating the user subscriptions of "
String msg = "Error occurred while processing SQL to update the user/role/group subscriptions of "
+ "application.";
log.error(msg);
throw new ApplicationManagementDAOException(msg, e);
@ -691,8 +690,8 @@ public class GenericSubscriptionDAOImpl extends AbstractDAOImpl implements Subsc
log.error(msg);
throw new ApplicationManagementDAOException(msg, e);
} catch (SQLException e) {
String msg = "Error occurred when obtaining database connection for updating the subscription status of the "
+ "device subscription.";
String msg = "Error occurred when processing SQL to update the subscription status of the device "
+ "subscription.";
log.error(msg);
throw new ApplicationManagementDAOException(msg, e);
}

@ -37,7 +37,7 @@
<Extension name="ApplicationStorageManager">
<ClassName>org.wso2.carbon.device.application.mgt.core.impl.ApplicationStorageManagerImpl</ClassName>
<Parameters>
<Parameter name="StoragePath">repository/resources/apps/</Parameter>
<Parameter name="StoragePath">/tmp/apps/</Parameter>
<Parameter name="MaxScreenShotCount">6</Parameter>
</Parameters>
</Extension>

@ -30,14 +30,8 @@ import org.wso2.carbon.apimgt.annotations.api.Scope;
import org.wso2.carbon.apimgt.annotations.api.Scopes;
import org.wso2.carbon.device.application.mgt.common.ErrorResponse;
import org.wso2.carbon.device.application.mgt.common.PaginationResult;
import org.wso2.carbon.device.application.mgt.common.response.Review;
import org.wso2.carbon.device.application.mgt.common.wrapper.ReviewWrapper;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
@ -69,13 +63,13 @@ tags = {
scopes = {
@Scope(
name = "Update a Review",
description = "Update a Review of applications.",
description = "Update a Review of application.",
key = "perm:admin:app:review:update",
permissions = {"/app-mgt/publisher/admin/review/update"}
),
@Scope(
name = "Get Review Details",
description = "Get review details of applications.",
description = "Get review details of application.",
key = "perm:admin:app:review:view",
permissions = {"/app-mgt/publisher/admin/review/view"}
)

@ -49,11 +49,11 @@ import javax.ws.rs.core.Response;
@SwaggerDefinition(
info = @Info(
version = "1.0.0",
title = "ApplicationDTO Storage Management Service",
title = "Application Storage Management Service",
extensions = {
@Extension(properties = {
@ExtensionProperty(name = "name", value = "ApplicationStorageManagementService"),
@ExtensionProperty(name = "context", value = "/api/application-mgt-store/v1.0/store-applications"),
@ExtensionProperty(name = "context", value = "/api/application-mgt-store/v1.0/applications"),
})
}
),
@ -65,7 +65,7 @@ import javax.ws.rs.core.Response;
@Scopes(
scopes = {
@Scope(
name = "Get ApplicationDTO Details",
name = "Get Application Details",
description = "Get application details",
key = "perm:app:store:view",
permissions = {"/app-mgt/store/application/view"}
@ -73,8 +73,8 @@ import javax.ws.rs.core.Response;
}
)
@Path("/applications")
@Api(value = "ApplicationDTO Management", description = "This API carries all app store management related operations " +
"such as get all the applications etc.")
@Api(value = "Application Management", description = "This API carries all app store management related operations such"
+ " as get all the applications etc.")
@Produces(MediaType.APPLICATION_JSON)
public interface ApplicationManagementAPI {
@ -89,7 +89,7 @@ public interface ApplicationManagementAPI {
httpMethod = "GET",
value = "get all applications",
notes = "This will get all applications",
tags = "ApplicationDTO Management",
tags = "Application Management",
extensions = {
@Extension(properties = {
@ExtensionProperty(name = SCOPE, value = "perm:app:store:view")
@ -129,7 +129,7 @@ public interface ApplicationManagementAPI {
httpMethod = "GET",
value = "get the application of requesting application type",
notes = "This will get the application identified by the application type and name, if exists",
tags = "ApplicationDTO Management",
tags = "Application Management",
extensions = {
@Extension(properties = {
@ExtensionProperty(name = SCOPE, value = "perm:app:store:view")
@ -144,7 +144,7 @@ public interface ApplicationManagementAPI {
response = ApplicationDTO.class),
@ApiResponse(
code = 404,
message = "ApplicationDTO not found"),
message = "Application not found"),
@ApiResponse(
code = 500,
message = "Internal Server Error. \n Error occurred while getting relevant application.",

@ -37,6 +37,10 @@
<param-name>doAuthentication</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<param-name>basicAuth</param-name>
<param-value>true</param-value>
</context-param>
<!--publish to apim-->
<context-param>

@ -324,9 +324,14 @@ public class CertificateGenerator {
KeyStoreReader keyStoreReader = new KeyStoreReader();
if (distinguishedName != null && !distinguishedName.isEmpty()) {
if (distinguishedName.contains("/CN=")) {
String[] dnSplits = distinguishedName.split("/CN=");
String commonNameExtracted = dnSplits[dnSplits.length - 1];
String[] dnSplits = distinguishedName.split("/");
for (String dnPart : dnSplits) {
if (dnPart.contains("CN=")) {
String commonNameExtracted = dnPart.replace("CN=", "");
lookUpCertificate = keyStoreReader.getCertificateBySerial(commonNameExtracted);
break;
}
}
} else {
LdapName ldapName;
try {

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
~
~ WSO2 Inc. 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.wso2.carbon.devicemgt</groupId>
<artifactId>application-mgt</artifactId>
<version>3.2.9-SNAPSHOT</version>
</parent>
<artifactId>io.entgra.device.mgt.ui</artifactId>
<version>3.2.9-SNAPSHOT</version>
<packaging>war</packaging>
<name>WSO2 Carbon - Device Management UI Component</name>
<url>http://wso2.org</url>
<description>This Component contains Device Management UI</description>
<dependencies>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<packagingExcludes>WEB-INF/lib/*cxf*.jar</packagingExcludes>
<warName>entgra</warName>
<webResources>
<resource>
<directory>${npm.output.directory}/dist</directory>
</resource>
<resource>
<directory>${npm.output.directory}/public</directory>
<targetPath>public</targetPath>
</resource>
</webResources>
</configuration>
</plugin>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>${frontend.mave.version}</version>
<configuration>
<workingDirectory>${npm.working.dir}</workingDirectory>
<!-- where to install npm -->
<installDirectory>${npm.install.dir}</installDirectory>
</configuration>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<phase>generate-resources</phase>
<configuration>
<nodeVersion>${node.version}</nodeVersion>
<npmVersion>${npm.version}</npmVersion>
</configuration>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<!-- Optional configuration which provides for running any npm command -->
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>prod</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run-script ${npm.build.command}</arguments>
</configuration>
<phase>generate-resources</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>platform-windows</id>
<activation>
<os>
<family>windows</family>
</os>
</activation>
<properties>
<!-- Override the executable names for Windows -->
<npm.executable>npm.cmd</npm.executable>
</properties>
</profile>
</profiles>
<properties>
<maven.test.skip>false</maven.test.skip>
<npm.executable>npm</npm.executable>
<npm.build.command>build_prod</npm.build.command>
<npm.working.dir>./react-app</npm.working.dir>
<npm.install.dir>./react-app/tmp</npm.install.dir>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<npm.output.directory>react-app</npm.output.directory>
</properties>
</project>

@ -0,0 +1,11 @@
module.exports = function (api) {
api.cache(true);
const presets = [ "@babel/preset-env",
"@babel/preset-react" ];
const plugins = ["@babel/plugin-proposal-class-properties"];
return {
presets,
plugins
};
};

@ -0,0 +1,86 @@
{
"name": "store",
"version": "1.0.0",
"description": "WSO2 IoT Server App Store",
"main": "App.js",
"proxy": "http://localhost:3001",
"repository": {
"type": "git",
"url": "git://github.com/wso2/carbon-devicemgt"
},
"license": "Apache License 2.0",
"dependencies": {
"acorn": "^6.2.0",
"antd": "^3.20.1",
"axios": "^0.18.1",
"d3": "^5.9.7",
"dagre": "^0.8.4",
"javascript-time-ago": "^2.0.1",
"keymirror": "^0.1.1",
"lodash.debounce": "^4.0.8",
"rc-viewer": "0.0.9",
"react-d3-graph": "^2.1.0",
"react-highlight-words": "^0.16.0",
"react-image-viewer-zoom": "^1.0.36",
"react-infinite-scroller": "^1.2.4",
"react-router": "^5.0.1",
"react-router-config": "^5.0.1",
"react-router-dom": "^5.0.1",
"react-scripts": "2.1.8",
"react-star-ratings": "^2.3.0",
"react-twemoji": "^0.2.3",
"react-virtualized": "^9.21.1",
"reqwest": "^2.0.5",
"storm-react-diagrams": "^5.2.1"
},
"devDependencies": {
"@babel/core": "^7.5.4",
"@babel/plugin-proposal-class-properties": "^7.5.0",
"@babel/preset-env": "^7.5.4",
"@babel/preset-react": "^7.0.0",
"@babel/register": "^7.4.4",
"babel-loader": "^8.0.6",
"body-parser": "^1.19.0",
"chai": "^4.1.2",
"css-loader": "^0.28.11",
"express": "^4.17.1",
"express-pino-logger": "^4.0.0",
"file-loader": "^2.0.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"img-loader": "^3.0.1",
"json-loader": "^0.5.7",
"less": "^3.9.0",
"less-loader": "^4.1.0",
"mini-css-extract-plugin": "^0.5.0",
"mocha": "^5.2.0",
"mock-local-storage": "^1.0.5",
"node-env-run": "^3.0.2",
"node-sass": "^4.12.0",
"nodemon": "^1.19.1",
"npm-run-all": "^4.1.5",
"pino-colada": "^1.4.5",
"postcss-loader": "^3.0.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-intl": "^2.9.0",
"sass-loader": "^6.0.7",
"style-loader": "^0.18.2",
"url-loader": "^1.1.2",
"webpack": "^4.35.3",
"webpack-cli": "^3.3.5",
"webpack-dev-server": "^3.7.2"
},
"scripts": {
"start": "webpack-dev-server --mode development --open",
"dev": "webpack --mode development",
"build": "webpack --mode production",
"watch": "webpack --watch --mode development",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"build_prod": "NODE_ENV=production NODE_OPTIONS=--max_old_space_size=4096 webpack -p --display errors-only --hide-modules",
"build_dev": "NODE_ENV=development webpack -d --watch ",
"server": "node-env-run server --exec nodemon | pino-colada",
"dev2": "run-p server start"
}
}

@ -0,0 +1,43 @@
{
"theme": {
"type": "default",
"value": "lightBaseTheme",
"logo" : "https://entgra.io/assets/images/svg/logo.svg",
"primaryColor": "rgb(24, 144, 255)"
},
"serverConfig": {
"invokerUri": "/ui-request-handler/invoke/application-mgt-store/v1.0",
"invoker": {
"uri": "/store-ui-request-handler/invoke",
"publisher": "/application-mgt-publisher/v1.0",
"store": "/application-mgt-store/v1.0",
"admin" : "",
"deviceMgt" : "/device-mgt/v1.0"
},
"loginUri": "/store-ui-request-handler/login",
"logoutUri": "/store-ui-request-handler/logout",
"platform": "store"
},
"defaultPlatformIcons": {
"default": {
"icon": "global",
"color": "#535c68",
"theme": "outlined"
},
"android": {
"icon": "android",
"color": "#7db343",
"theme": "filled"
},
"ios": {
"icon": "apple",
"color": "#535c68",
"theme": "filled"
},
"windows": {
"icon": "windows",
"color": "#008cc4",
"theme": "filled"
}
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,15 @@
{
"short_name": "App Store",
"name": "WSO2 IoT App Store",
"icons": [
{
"src": "images/favicon.png",
"sizes": "16x16",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 443 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -0,0 +1,798 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 232 81" style="enable-background:new 0 0 232 81;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{clip-path:url(#SVGID_2_);fill:#308BD6;}
.st2{clip-path:url(#SVGID_2_);fill:#318CD6;}
.st3{clip-path:url(#SVGID_2_);fill:#328DD7;}
.st4{clip-path:url(#SVGID_2_);fill:#338ED7;}
.st5{clip-path:url(#SVGID_2_);fill:#348FD8;}
.st6{clip-path:url(#SVGID_2_);fill:#3590D8;}
.st7{clip-path:url(#SVGID_2_);fill:#3691D9;}
.st8{clip-path:url(#SVGID_2_);fill:#3792D9;}
.st9{clip-path:url(#SVGID_2_);fill:#3893DA;}
.st10{clip-path:url(#SVGID_2_);fill:#3994DA;}
.st11{clip-path:url(#SVGID_2_);fill:#3A95DA;}
.st12{clip-path:url(#SVGID_2_);fill:#3B96DB;}
.st13{clip-path:url(#SVGID_2_);fill:#3C97DB;}
.st14{clip-path:url(#SVGID_2_);fill:#3D98DC;}
.st15{clip-path:url(#SVGID_2_);fill:#3E99DC;}
.st16{clip-path:url(#SVGID_2_);fill:#3F9ADD;}
.st17{clip-path:url(#SVGID_2_);fill:#409BDD;}
.st18{clip-path:url(#SVGID_2_);fill:#419CDE;}
.st19{clip-path:url(#SVGID_2_);fill:#429DDE;}
.st20{clip-path:url(#SVGID_2_);fill:#439EDE;}
.st21{clip-path:url(#SVGID_2_);fill:#449FDF;}
.st22{clip-path:url(#SVGID_2_);fill:#45A0DF;}
.st23{clip-path:url(#SVGID_2_);fill:#46A1E0;}
.st24{clip-path:url(#SVGID_2_);fill:#47A2E0;}
.st25{clip-path:url(#SVGID_2_);fill:#48A4E1;}
.st26{clip-path:url(#SVGID_2_);fill:#49A5E1;}
.st27{clip-path:url(#SVGID_2_);fill:#4AA6E2;}
.st28{clip-path:url(#SVGID_2_);fill:#4BA7E2;}
.st29{clip-path:url(#SVGID_2_);fill:#4CA8E3;}
.st30{clip-path:url(#SVGID_2_);fill:#4DA9E3;}
.st31{clip-path:url(#SVGID_2_);fill:#4EAAE3;}
.st32{clip-path:url(#SVGID_2_);fill:#4FABE4;}
.st33{clip-path:url(#SVGID_2_);fill:#50ACE4;}
.st34{clip-path:url(#SVGID_2_);fill:#51ADE5;}
.st35{clip-path:url(#SVGID_2_);fill:#52AEE5;}
.st36{clip-path:url(#SVGID_2_);fill:#53AFE6;}
.st37{clip-path:url(#SVGID_2_);fill:#54B0E6;}
.st38{clip-path:url(#SVGID_2_);fill:#55B1E7;}
.st39{clip-path:url(#SVGID_2_);fill:#56B2E7;}
.st40{clip-path:url(#SVGID_2_);fill:#57B3E7;}
.st41{clip-path:url(#SVGID_2_);fill:#58B4E8;}
.st42{clip-path:url(#SVGID_2_);fill:#59B5E8;}
.st43{clip-path:url(#SVGID_2_);fill:#5AB6E9;}
.st44{clip-path:url(#SVGID_2_);fill:#5BB7E9;}
.st45{clip-path:url(#SVGID_2_);fill:#5CB8EA;}
.st46{clip-path:url(#SVGID_2_);fill:#5DB9EA;}
.st47{clip-path:url(#SVGID_2_);fill:#5EBAEB;}
.st48{clip-path:url(#SVGID_2_);fill:#5FBBEB;}
.st49{clip-path:url(#SVGID_4_);fill:#2E5E87;}
.st50{clip-path:url(#SVGID_4_);fill:#2E5F88;}
.st51{clip-path:url(#SVGID_4_);fill:#2F5F89;}
.st52{clip-path:url(#SVGID_4_);fill:#2F608A;}
.st53{clip-path:url(#SVGID_4_);fill:#30618B;}
.st54{clip-path:url(#SVGID_4_);fill:#30628C;}
.st55{clip-path:url(#SVGID_4_);fill:#31628D;}
.st56{clip-path:url(#SVGID_4_);fill:#31638E;}
.st57{clip-path:url(#SVGID_4_);fill:#31648F;}
.st58{clip-path:url(#SVGID_4_);fill:#326590;}
.st59{clip-path:url(#SVGID_4_);fill:#326591;}
.st60{clip-path:url(#SVGID_4_);fill:#336692;}
.st61{clip-path:url(#SVGID_4_);fill:#336793;}
.st62{clip-path:url(#SVGID_4_);fill:#346894;}
.st63{clip-path:url(#SVGID_4_);fill:#346895;}
.st64{clip-path:url(#SVGID_4_);fill:#356996;}
.st65{clip-path:url(#SVGID_4_);fill:#356A97;}
.st66{clip-path:url(#SVGID_4_);fill:#356B98;}
.st67{clip-path:url(#SVGID_4_);fill:#366B99;}
.st68{clip-path:url(#SVGID_4_);fill:#366C9A;}
.st69{clip-path:url(#SVGID_4_);fill:#376D9B;}
.st70{clip-path:url(#SVGID_4_);fill:#376E9C;}
.st71{clip-path:url(#SVGID_4_);fill:#386E9D;}
.st72{clip-path:url(#SVGID_4_);fill:#386F9E;}
.st73{clip-path:url(#SVGID_4_);fill:#38709F;}
.st74{clip-path:url(#SVGID_4_);fill:#3971A0;}
.st75{clip-path:url(#SVGID_4_);fill:#3971A1;}
.st76{clip-path:url(#SVGID_4_);fill:#3A72A2;}
.st77{clip-path:url(#SVGID_4_);fill:#3A73A3;}
.st78{clip-path:url(#SVGID_4_);fill:#3B74A4;}
.st79{clip-path:url(#SVGID_4_);fill:#3B74A5;}
.st80{clip-path:url(#SVGID_4_);fill:#3C75A7;}
.st81{clip-path:url(#SVGID_4_);fill:#3C76A8;}
.st82{clip-path:url(#SVGID_4_);fill:#3C76A9;}
.st83{clip-path:url(#SVGID_4_);fill:#3D77AA;}
.st84{clip-path:url(#SVGID_4_);fill:#3D78AB;}
.st85{clip-path:url(#SVGID_4_);fill:#3E79AC;}
.st86{clip-path:url(#SVGID_4_);fill:#3E79AD;}
.st87{clip-path:url(#SVGID_4_);fill:#3F7AAE;}
.st88{clip-path:url(#SVGID_4_);fill:#3F7BAF;}
.st89{clip-path:url(#SVGID_4_);fill:#3F7CB0;}
.st90{clip-path:url(#SVGID_4_);fill:#407CB1;}
.st91{clip-path:url(#SVGID_4_);fill:#407DB2;}
.st92{clip-path:url(#SVGID_4_);fill:#417EB3;}
.st93{clip-path:url(#SVGID_4_);fill:#417FB4;}
.st94{clip-path:url(#SVGID_4_);fill:#427FB5;}
.st95{clip-path:url(#SVGID_4_);fill:#4280B6;}
.st96{clip-path:url(#SVGID_4_);fill:#4281B7;}
.st97{clip-path:url(#SVGID_4_);fill:#4382B8;}
.st98{clip-path:url(#SVGID_4_);fill:#4382B9;}
.st99{clip-path:url(#SVGID_4_);fill:#4483BA;}
.st100{clip-path:url(#SVGID_4_);fill:#4484BB;}
.st101{clip-path:url(#SVGID_4_);fill:#4585BC;}
.st102{clip-path:url(#SVGID_4_);fill:#4585BD;}
.st103{clip-path:url(#SVGID_4_);fill:#4686BE;}
.st104{clip-path:url(#SVGID_4_);fill:#4687BF;}
.st105{clip-path:url(#SVGID_4_);fill:#4688C0;}
.st106{clip-path:url(#SVGID_4_);fill:#4788C1;}
.st107{clip-path:url(#SVGID_4_);fill:#4789C2;}
.st108{clip-path:url(#SVGID_4_);fill:#488AC3;}
.st109{clip-path:url(#SVGID_4_);fill:#488BC4;}
.st110{clip-path:url(#SVGID_4_);fill:#498BC5;}
.st111{clip-path:url(#SVGID_4_);fill:#498CC6;}
.st112{clip-path:url(#SVGID_6_);fill:#4CE8C6;}
.st113{clip-path:url(#SVGID_6_);fill:#4BE8C7;}
.st114{clip-path:url(#SVGID_6_);fill:#4AE7C7;}
.st115{clip-path:url(#SVGID_6_);fill:#49E7C8;}
.st116{clip-path:url(#SVGID_6_);fill:#48E6C8;}
.st117{clip-path:url(#SVGID_6_);fill:#47E6C9;}
.st118{clip-path:url(#SVGID_6_);fill:#46E6C9;}
.st119{clip-path:url(#SVGID_6_);fill:#45E5CA;}
.st120{clip-path:url(#SVGID_6_);fill:#44E5CB;}
.st121{clip-path:url(#SVGID_6_);fill:#43E4CB;}
.st122{clip-path:url(#SVGID_6_);fill:#42E4CC;}
.st123{clip-path:url(#SVGID_6_);fill:#41E4CC;}
.st124{clip-path:url(#SVGID_6_);fill:#40E3CD;}
.st125{clip-path:url(#SVGID_6_);fill:#3FE3CE;}
.st126{clip-path:url(#SVGID_6_);fill:#3EE2CE;}
.st127{clip-path:url(#SVGID_6_);fill:#3DE2CF;}
.st128{clip-path:url(#SVGID_6_);fill:#3CE2CF;}
.st129{clip-path:url(#SVGID_6_);fill:#3BE1D0;}
.st130{clip-path:url(#SVGID_6_);fill:#3AE1D0;}
.st131{clip-path:url(#SVGID_6_);fill:#39E0D1;}
.st132{clip-path:url(#SVGID_6_);fill:#38E0D2;}
.st133{clip-path:url(#SVGID_6_);fill:#37E0D2;}
.st134{clip-path:url(#SVGID_6_);fill:#36DFD3;}
.st135{clip-path:url(#SVGID_6_);fill:#35DFD3;}
.st136{clip-path:url(#SVGID_6_);fill:#34DED4;}
.st137{clip-path:url(#SVGID_6_);fill:#33DED5;}
.st138{clip-path:url(#SVGID_6_);fill:#31DED5;}
.st139{clip-path:url(#SVGID_6_);fill:#30DDD6;}
.st140{clip-path:url(#SVGID_6_);fill:#2FDDD6;}
.st141{clip-path:url(#SVGID_6_);fill:#2EDCD7;}
.st142{clip-path:url(#SVGID_6_);fill:#2DDCD7;}
.st143{clip-path:url(#SVGID_6_);fill:#2CDCD8;}
.st144{clip-path:url(#SVGID_6_);fill:#2BDBD9;}
.st145{clip-path:url(#SVGID_6_);fill:#2ADBD9;}
.st146{clip-path:url(#SVGID_6_);fill:#29DADA;}
.st147{clip-path:url(#SVGID_6_);fill:#28DADA;}
.st148{clip-path:url(#SVGID_6_);fill:#27DADB;}
.st149{clip-path:url(#SVGID_6_);fill:#26D9DB;}
.st150{clip-path:url(#SVGID_6_);fill:#25D9DC;}
.st151{clip-path:url(#SVGID_6_);fill:#24D8DD;}
.st152{clip-path:url(#SVGID_6_);fill:#23D8DD;}
.st153{clip-path:url(#SVGID_6_);fill:#22D8DE;}
.st154{clip-path:url(#SVGID_6_);fill:#21D7DE;}
.st155{clip-path:url(#SVGID_6_);fill:#20D7DF;}
.st156{clip-path:url(#SVGID_6_);fill:#1FD6E0;}
.st157{clip-path:url(#SVGID_6_);fill:#1ED6E0;}
.st158{clip-path:url(#SVGID_6_);fill:#1DD6E1;}
.st159{clip-path:url(#SVGID_6_);fill:#1CD5E1;}
.st160{clip-path:url(#SVGID_6_);fill:#1BD5E2;}
.st161{clip-path:url(#SVGID_6_);fill:#1AD4E2;}
.st162{clip-path:url(#SVGID_6_);fill:#19D4E3;}
.st163{opacity:0.4;}
.st164{clip-path:url(#SVGID_8_);fill:#4CE8C6;}
.st165{clip-path:url(#SVGID_8_);fill:#4BE8C7;}
.st166{clip-path:url(#SVGID_8_);fill:#4AE7C7;}
.st167{clip-path:url(#SVGID_8_);fill:#49E7C8;}
.st168{clip-path:url(#SVGID_8_);fill:#48E6C8;}
.st169{clip-path:url(#SVGID_8_);fill:#47E6C9;}
.st170{clip-path:url(#SVGID_8_);fill:#46E6C9;}
.st171{clip-path:url(#SVGID_8_);fill:#45E5CA;}
.st172{clip-path:url(#SVGID_8_);fill:#44E5CB;}
.st173{clip-path:url(#SVGID_8_);fill:#43E4CB;}
.st174{clip-path:url(#SVGID_8_);fill:#42E4CC;}
.st175{clip-path:url(#SVGID_8_);fill:#41E4CC;}
.st176{clip-path:url(#SVGID_8_);fill:#40E3CD;}
.st177{clip-path:url(#SVGID_8_);fill:#3FE3CE;}
.st178{clip-path:url(#SVGID_8_);fill:#3EE2CE;}
.st179{clip-path:url(#SVGID_8_);fill:#3DE2CF;}
.st180{clip-path:url(#SVGID_8_);fill:#3CE2CF;}
.st181{clip-path:url(#SVGID_8_);fill:#3BE1D0;}
.st182{clip-path:url(#SVGID_8_);fill:#3AE1D0;}
.st183{clip-path:url(#SVGID_8_);fill:#39E0D1;}
.st184{clip-path:url(#SVGID_8_);fill:#38E0D2;}
.st185{clip-path:url(#SVGID_8_);fill:#37E0D2;}
.st186{clip-path:url(#SVGID_8_);fill:#36DFD3;}
.st187{clip-path:url(#SVGID_8_);fill:#35DFD3;}
.st188{clip-path:url(#SVGID_8_);fill:#34DED4;}
.st189{clip-path:url(#SVGID_8_);fill:#33DED5;}
.st190{clip-path:url(#SVGID_8_);fill:#31DED5;}
.st191{clip-path:url(#SVGID_8_);fill:#30DDD6;}
.st192{clip-path:url(#SVGID_8_);fill:#2FDDD6;}
.st193{clip-path:url(#SVGID_8_);fill:#2EDCD7;}
.st194{clip-path:url(#SVGID_8_);fill:#2DDCD7;}
.st195{clip-path:url(#SVGID_8_);fill:#2CDCD8;}
.st196{clip-path:url(#SVGID_8_);fill:#2BDBD9;}
.st197{clip-path:url(#SVGID_8_);fill:#2ADBD9;}
.st198{clip-path:url(#SVGID_8_);fill:#29DADA;}
.st199{clip-path:url(#SVGID_8_);fill:#28DADA;}
.st200{clip-path:url(#SVGID_8_);fill:#27DADB;}
.st201{clip-path:url(#SVGID_8_);fill:#26D9DB;}
.st202{clip-path:url(#SVGID_8_);fill:#25D9DC;}
.st203{clip-path:url(#SVGID_8_);fill:#24D8DD;}
.st204{clip-path:url(#SVGID_8_);fill:#23D8DD;}
.st205{clip-path:url(#SVGID_8_);fill:#22D8DE;}
.st206{clip-path:url(#SVGID_8_);fill:#21D7DE;}
.st207{clip-path:url(#SVGID_8_);fill:#20D7DF;}
.st208{clip-path:url(#SVGID_8_);fill:#1FD6E0;}
.st209{clip-path:url(#SVGID_8_);fill:#1ED6E0;}
.st210{clip-path:url(#SVGID_8_);fill:#1DD6E1;}
.st211{clip-path:url(#SVGID_8_);fill:#1CD5E1;}
.st212{clip-path:url(#SVGID_8_);fill:#1BD5E2;}
.st213{clip-path:url(#SVGID_8_);fill:#1AD4E2;}
.st214{clip-path:url(#SVGID_8_);fill:#19D4E3;}
.st215{opacity:0.5;}
.st216{clip-path:url(#SVGID_10_);fill:#316490;}
.st217{clip-path:url(#SVGID_10_);fill:#316591;}
.st218{clip-path:url(#SVGID_10_);fill:#326692;}
.st219{clip-path:url(#SVGID_10_);fill:#326693;}
.st220{clip-path:url(#SVGID_10_);fill:#336794;}
.st221{clip-path:url(#SVGID_10_);fill:#336895;}
.st222{clip-path:url(#SVGID_10_);fill:#346996;}
.st223{clip-path:url(#SVGID_10_);fill:#346997;}
.st224{clip-path:url(#SVGID_10_);fill:#356A98;}
.st225{clip-path:url(#SVGID_10_);fill:#356B99;}
.st226{clip-path:url(#SVGID_10_);fill:#366C9A;}
.st227{clip-path:url(#SVGID_10_);fill:#366C9B;}
.st228{clip-path:url(#SVGID_10_);fill:#366D9C;}
.st229{clip-path:url(#SVGID_10_);fill:#376E9D;}
.st230{clip-path:url(#SVGID_10_);fill:#376F9E;}
.st231{clip-path:url(#SVGID_10_);fill:#386F9F;}
.st232{clip-path:url(#SVGID_10_);fill:#3870A0;}
.st233{clip-path:url(#SVGID_10_);fill:#3971A1;}
.st234{clip-path:url(#SVGID_10_);fill:#3972A2;}
.st235{clip-path:url(#SVGID_10_);fill:#3A72A3;}
.st236{clip-path:url(#SVGID_10_);fill:#3A73A4;}
.st237{clip-path:url(#SVGID_10_);fill:#3B74A5;}
.st238{clip-path:url(#SVGID_10_);fill:#3B75A6;}
.st239{clip-path:url(#SVGID_10_);fill:#3B75A7;}
.st240{clip-path:url(#SVGID_10_);fill:#3C76A8;}
.st241{clip-path:url(#SVGID_10_);fill:#3C77A9;}
.st242{clip-path:url(#SVGID_10_);fill:#3D78AA;}
.st243{clip-path:url(#SVGID_10_);fill:#3D78AC;}
.st244{clip-path:url(#SVGID_10_);fill:#3E79AD;}
.st245{clip-path:url(#SVGID_10_);fill:#3E7AAE;}
.st246{clip-path:url(#SVGID_10_);fill:#3F7BAF;}
.st247{clip-path:url(#SVGID_10_);fill:#3F7BB0;}
.st248{clip-path:url(#SVGID_10_);fill:#3F7CB1;}
.st249{clip-path:url(#SVGID_10_);fill:#407DB2;}
.st250{clip-path:url(#SVGID_10_);fill:#407EB3;}
.st251{clip-path:url(#SVGID_10_);fill:#417EB4;}
.st252{clip-path:url(#SVGID_10_);fill:#417FB5;}
.st253{clip-path:url(#SVGID_10_);fill:#4280B6;}
.st254{clip-path:url(#SVGID_10_);fill:#4281B7;}
.st255{clip-path:url(#SVGID_10_);fill:#4381B8;}
.st256{clip-path:url(#SVGID_10_);fill:#4382B9;}
.st257{clip-path:url(#SVGID_10_);fill:#4483BA;}
.st258{clip-path:url(#SVGID_10_);fill:#4484BB;}
.st259{clip-path:url(#SVGID_10_);fill:#4484BC;}
.st260{clip-path:url(#SVGID_10_);fill:#4585BD;}
.st261{clip-path:url(#SVGID_10_);fill:#4586BE;}
.st262{clip-path:url(#SVGID_10_);fill:#4687BF;}
.st263{clip-path:url(#SVGID_10_);fill:#4687C0;}
.st264{clip-path:url(#SVGID_10_);fill:#4788C1;}
.st265{clip-path:url(#SVGID_10_);fill:#4789C2;}
.st266{clip-path:url(#SVGID_10_);fill:#488AC3;}
.st267{clip-path:url(#SVGID_10_);fill:#488AC4;}
.st268{clip-path:url(#SVGID_10_);fill:#498BC5;}
.st269{clip-path:url(#SVGID_10_);fill:#498CC6;}
.st270{clip-path:url(#SVGID_12_);fill:#5FBBEB;}
.st271{clip-path:url(#SVGID_12_);fill:#5EBAEB;}
.st272{clip-path:url(#SVGID_12_);fill:#5DB9EA;}
.st273{clip-path:url(#SVGID_12_);fill:#5CB8EA;}
.st274{clip-path:url(#SVGID_12_);fill:#5BB7E9;}
.st275{clip-path:url(#SVGID_12_);fill:#5AB6E9;}
.st276{clip-path:url(#SVGID_12_);fill:#59B5E8;}
.st277{clip-path:url(#SVGID_12_);fill:#58B4E8;}
.st278{clip-path:url(#SVGID_12_);fill:#57B3E7;}
.st279{clip-path:url(#SVGID_12_);fill:#56B2E7;}
.st280{clip-path:url(#SVGID_12_);fill:#55B1E7;}
.st281{clip-path:url(#SVGID_12_);fill:#54B0E6;}
.st282{clip-path:url(#SVGID_12_);fill:#53AFE6;}
.st283{clip-path:url(#SVGID_12_);fill:#52AEE5;}
.st284{clip-path:url(#SVGID_12_);fill:#51ADE5;}
.st285{clip-path:url(#SVGID_12_);fill:#50ACE4;}
.st286{clip-path:url(#SVGID_12_);fill:#4FABE4;}
.st287{clip-path:url(#SVGID_12_);fill:#4EAAE3;}
.st288{clip-path:url(#SVGID_12_);fill:#4DA9E3;}
.st289{clip-path:url(#SVGID_12_);fill:#4CA8E3;}
.st290{clip-path:url(#SVGID_12_);fill:#4BA7E2;}
.st291{clip-path:url(#SVGID_12_);fill:#4AA6E2;}
.st292{clip-path:url(#SVGID_12_);fill:#49A5E1;}
.st293{clip-path:url(#SVGID_12_);fill:#48A4E1;}
.st294{clip-path:url(#SVGID_12_);fill:#47A2E0;}
.st295{clip-path:url(#SVGID_12_);fill:#46A1E0;}
.st296{clip-path:url(#SVGID_12_);fill:#45A0DF;}
.st297{clip-path:url(#SVGID_12_);fill:#449FDF;}
.st298{clip-path:url(#SVGID_12_);fill:#439EDE;}
.st299{clip-path:url(#SVGID_12_);fill:#429DDE;}
.st300{clip-path:url(#SVGID_12_);fill:#419CDE;}
.st301{clip-path:url(#SVGID_12_);fill:#409BDD;}
.st302{clip-path:url(#SVGID_12_);fill:#3F9ADD;}
.st303{clip-path:url(#SVGID_12_);fill:#3E99DC;}
.st304{clip-path:url(#SVGID_12_);fill:#3D98DC;}
.st305{clip-path:url(#SVGID_12_);fill:#3C97DB;}
.st306{clip-path:url(#SVGID_12_);fill:#3B96DB;}
.st307{clip-path:url(#SVGID_12_);fill:#3A95DA;}
.st308{clip-path:url(#SVGID_12_);fill:#3994DA;}
.st309{clip-path:url(#SVGID_12_);fill:#3893DA;}
.st310{clip-path:url(#SVGID_12_);fill:#3792D9;}
.st311{clip-path:url(#SVGID_12_);fill:#3691D9;}
.st312{clip-path:url(#SVGID_12_);fill:#3590D8;}
.st313{clip-path:url(#SVGID_12_);fill:#348FD8;}
.st314{clip-path:url(#SVGID_12_);fill:#338ED7;}
.st315{clip-path:url(#SVGID_12_);fill:#328DD7;}
.st316{clip-path:url(#SVGID_12_);fill:#318CD6;}
.st317{clip-path:url(#SVGID_12_);fill:#308BD6;}
.st318{fill:#316490;}
</style>
<path class="st0" d="M224,81H8c-4.4,0-8-3.6-8-8V8c0-4.4,3.6-8,8-8h216c4.4,0,8,3.6,8,8v65C232,77.4,228.4,81,224,81z"/>
<g>
<g>
<g>
<g>
<g>
<defs>
<polygon id="SVGID_1_" points="15.2,15 15.2,62.9 59.2,56 59.2,8.1 "/>
</defs>
<clipPath id="SVGID_2_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<polygon class="st1" points="56.7,62.9 59.2,56 59.2,62.9 "/>
<polygon class="st1" points="55.7,62.9 59.2,53.1 59.2,56 56.7,62.9 "/>
<polygon class="st2" points="54.6,62.9 59.2,50.3 59.2,53.1 55.7,62.9 "/>
<polygon class="st3" points="53.6,62.9 59.2,47.4 59.2,50.3 54.6,62.9 "/>
<polygon class="st4" points="52.6,62.9 59.2,44.6 59.2,47.4 53.6,62.9 "/>
<polygon class="st5" points="51.5,62.9 59.2,41.7 59.2,44.6 52.6,62.9 "/>
<polygon class="st6" points="50.5,62.9 59.2,38.9 59.2,41.7 51.5,62.9 "/>
<polygon class="st7" points="49.4,62.9 59.2,36 59.2,38.9 50.5,62.9 "/>
<polygon class="st8" points="48.4,62.9 59.2,33.1 59.2,36 49.4,62.9 "/>
<polygon class="st9" points="47.4,62.9 59.2,30.3 59.2,33.1 48.4,62.9 "/>
<polygon class="st10" points="46.3,62.9 59.2,27.4 59.2,30.3 47.4,62.9 "/>
<polygon class="st11" points="45.3,62.9 59.2,24.6 59.2,27.4 46.3,62.9 "/>
<polygon class="st12" points="44.2,62.9 59.2,21.7 59.2,24.6 45.3,62.9 "/>
<polygon class="st13" points="43.2,62.9 59.2,18.9 59.2,21.7 44.2,62.9 "/>
<polygon class="st14" points="42.2,62.9 59.2,16 59.2,18.9 43.2,62.9 "/>
<polygon class="st15" points="41.1,62.9 59.2,13.2 59.2,16 42.2,62.9 "/>
<polygon class="st16" points="40.1,62.9 59.2,10.3 59.2,13.2 41.1,62.9 "/>
<polygon class="st17" points="39,62.9 59,8.1 59.2,8.1 59.2,10.3 40.1,62.9 "/>
<polygon class="st18" points="38,62.9 57.9,8.1 59,8.1 39,62.9 "/>
<polygon class="st19" points="37,62.9 56.9,8.1 57.9,8.1 38,62.9 "/>
<polygon class="st20" points="35.9,62.9 55.9,8.1 56.9,8.1 37,62.9 "/>
<polygon class="st21" points="34.9,62.9 54.8,8.1 55.9,8.1 35.9,62.9 "/>
<polygon class="st22" points="33.8,62.9 53.8,8.1 54.8,8.1 34.9,62.9 "/>
<polygon class="st23" points="32.8,62.9 52.7,8.1 53.8,8.1 33.8,62.9 "/>
<polygon class="st24" points="31.8,62.9 51.7,8.1 52.7,8.1 32.8,62.9 "/>
<polygon class="st25" points="30.7,62.9 50.7,8.1 51.7,8.1 31.8,62.9 "/>
<polygon class="st26" points="29.7,62.9 49.6,8.1 50.7,8.1 30.7,62.9 "/>
<polygon class="st27" points="28.7,62.9 48.6,8.1 49.6,8.1 29.7,62.9 "/>
<polygon class="st28" points="27.6,62.9 47.5,8.1 48.6,8.1 28.7,62.9 "/>
<polygon class="st29" points="26.6,62.9 46.5,8.1 47.5,8.1 27.6,62.9 "/>
<polygon class="st30" points="25.5,62.9 45.5,8.1 46.5,8.1 26.6,62.9 "/>
<polygon class="st31" points="24.5,62.9 44.4,8.1 45.5,8.1 25.5,62.9 "/>
<polygon class="st32" points="23.5,62.9 43.4,8.1 44.4,8.1 24.5,62.9 "/>
<polygon class="st33" points="22.4,62.9 42.3,8.1 43.4,8.1 23.5,62.9 "/>
<polygon class="st34" points="21.4,62.9 41.3,8.1 42.3,8.1 22.4,62.9 "/>
<polygon class="st35" points="20.3,62.9 40.3,8.1 41.3,8.1 21.4,62.9 "/>
<polygon class="st36" points="19.3,62.9 39.2,8.1 40.3,8.1 20.3,62.9 "/>
<polygon class="st37" points="18.3,62.9 38.2,8.1 39.2,8.1 19.3,62.9 "/>
<polygon class="st38" points="17.2,62.9 37.1,8.1 38.2,8.1 18.3,62.9 "/>
<polygon class="st39" points="16.2,62.9 36.1,8.1 37.1,8.1 17.2,62.9 "/>
<polygon class="st40" points="15.2,62.7 35.1,8.1 36.1,8.1 16.2,62.9 15.2,62.9 "/>
<polygon class="st41" points="15.2,59.9 34,8.1 35.1,8.1 15.2,62.7 "/>
<polygon class="st42" points="15.2,57 33,8.1 34,8.1 15.2,59.9 "/>
<polygon class="st43" points="15.2,54.2 32,8.1 33,8.1 15.2,57 "/>
<polygon class="st44" points="15.2,51.3 30.9,8.1 32,8.1 15.2,54.2 "/>
<polygon class="st45" points="15.2,48.5 29.9,8.1 30.9,8.1 15.2,51.3 "/>
<polygon class="st46" points="15.2,45.6 28.8,8.1 29.9,8.1 15.2,48.5 "/>
<polygon class="st47" points="15.2,42.7 27.8,8.1 28.8,8.1 15.2,45.6 "/>
<polygon class="st48" points="15.2,39.9 26.8,8.1 27.8,8.1 15.2,42.7 "/>
<polygon class="st48" points="26.8,8.1 15.2,39.9 15.2,8.1 "/>
</g>
</g>
</g>
<g>
<g>
<g>
<defs>
<polygon id="SVGID_3_" points="83,68.7 83,20.8 59.2,8.1 59.2,56 "/>
</defs>
<clipPath id="SVGID_4_">
<use xlink:href="#SVGID_3_" style="overflow:visible;"/>
</clipPath>
<polygon class="st49" points="83,20.8 78.4,8.1 83,8.1 "/>
<polygon class="st49" points="83,22 77.9,8.1 78.4,8.1 83,20.8 "/>
<polygon class="st50" points="83,23.1 77.5,8.1 77.9,8.1 83,22 "/>
<polygon class="st51" points="83,24.3 77.1,8.1 77.5,8.1 83,23.1 "/>
<polygon class="st52" points="83,25.4 76.7,8.1 77.1,8.1 83,24.3 "/>
<polygon class="st53" points="83,26.6 76.3,8.1 76.7,8.1 83,25.4 "/>
<polygon class="st54" points="83,27.7 75.9,8.1 76.3,8.1 83,26.6 "/>
<polygon class="st55" points="83,28.8 75.4,8.1 75.9,8.1 83,27.7 "/>
<polygon class="st56" points="83,30 75,8.1 75.4,8.1 83,28.8 "/>
<polygon class="st57" points="83,31.1 74.6,8.1 75,8.1 83,30 "/>
<polygon class="st58" points="83,32.3 74.2,8.1 74.6,8.1 83,31.1 "/>
<polygon class="st59" points="83,33.4 73.8,8.1 74.2,8.1 83,32.3 "/>
<polygon class="st60" points="83,34.6 73.4,8.1 73.8,8.1 83,33.4 "/>
<polygon class="st61" points="83,35.7 72.9,8.1 73.4,8.1 83,34.6 "/>
<polygon class="st62" points="83,36.9 72.5,8.1 72.9,8.1 83,35.7 "/>
<polygon class="st63" points="83,38 72.1,8.1 72.5,8.1 83,36.9 "/>
<polygon class="st64" points="83,39.1 71.7,8.1 72.1,8.1 83,38 "/>
<polygon class="st65" points="83,40.3 71.3,8.1 71.7,8.1 83,39.1 "/>
<polygon class="st66" points="83,41.4 70.9,8.1 71.3,8.1 83,40.3 "/>
<polygon class="st67" points="83,42.6 70.4,8.1 70.9,8.1 83,41.4 "/>
<polygon class="st68" points="83,43.7 70,8.1 70.4,8.1 83,42.6 "/>
<polygon class="st69" points="83,44.9 69.6,8.1 70,8.1 83,43.7 "/>
<polygon class="st70" points="83,46 69.2,8.1 69.6,8.1 83,44.9 "/>
<polygon class="st71" points="83,47.2 68.8,8.1 69.2,8.1 83,46 "/>
<polygon class="st72" points="83,48.3 68.4,8.1 68.8,8.1 83,47.2 "/>
<polygon class="st73" points="83,49.5 67.9,8.1 68.4,8.1 83,48.3 "/>
<polygon class="st74" points="83,50.6 67.5,8.1 67.9,8.1 83,49.5 "/>
<polygon class="st75" points="83,51.7 67.1,8.1 67.5,8.1 83,50.6 "/>
<polygon class="st76" points="83,52.9 66.7,8.1 67.1,8.1 83,51.7 "/>
<polygon class="st77" points="83,54 66.3,8.1 66.7,8.1 83,52.9 "/>
<polygon class="st78" points="83,55.2 65.9,8.1 66.3,8.1 83,54 "/>
<polygon class="st79" points="83,56.3 65.4,8.1 65.9,8.1 83,55.2 "/>
<polygon class="st80" points="83,57.5 65,8.1 65.4,8.1 83,56.3 "/>
<polygon class="st81" points="83,58.6 64.6,8.1 65,8.1 83,57.5 "/>
<polygon class="st82" points="83,59.8 64.2,8.1 64.6,8.1 83,58.6 "/>
<polygon class="st83" points="83,60.9 63.8,8.1 64.2,8.1 83,59.8 "/>
<polygon class="st84" points="83,62 63.3,8.1 63.8,8.1 83,60.9 "/>
<polygon class="st85" points="83,63.2 62.9,8.1 63.3,8.1 83,62 "/>
<polygon class="st86" points="83,64.3 62.5,8.1 62.9,8.1 83,63.2 "/>
<polygon class="st87" points="83,65.5 62.1,8.1 62.5,8.1 83,64.3 "/>
<polygon class="st88" points="83,66.6 61.7,8.1 62.1,8.1 83,65.5 "/>
<polygon class="st89" points="83,67.8 61.3,8.1 61.7,8.1 83,66.6 "/>
<polygon class="st90" points="82.9,68.7 60.8,8.1 61.3,8.1 83,67.8 83,68.7 "/>
<polygon class="st91" points="82.5,68.7 60.4,8.1 60.8,8.1 82.9,68.7 "/>
<polygon class="st92" points="82.1,68.7 60,8.1 60.4,8.1 82.5,68.7 "/>
<polygon class="st93" points="81.6,68.7 59.6,8.1 60,8.1 82.1,68.7 "/>
<polygon class="st94" points="81.2,68.7 59.2,8.1 59.2,8.1 59.6,8.1 81.6,68.7 "/>
<polygon class="st95" points="80.8,68.7 59.2,9.3 59.2,8.1 81.2,68.7 "/>
<polygon class="st96" points="80.4,68.7 59.2,10.4 59.2,9.3 80.8,68.7 "/>
<polygon class="st97" points="80,68.7 59.2,11.6 59.2,10.4 80.4,68.7 "/>
<polygon class="st98" points="79.6,68.7 59.2,12.7 59.2,11.6 80,68.7 "/>
<polygon class="st99" points="79.1,68.7 59.2,13.8 59.2,12.7 79.6,68.7 "/>
<polygon class="st100" points="78.7,68.7 59.2,15 59.2,13.8 79.1,68.7 "/>
<polygon class="st101" points="78.3,68.7 59.2,16.1 59.2,15 78.7,68.7 "/>
<polygon class="st102" points="77.9,68.7 59.2,17.3 59.2,16.1 78.3,68.7 "/>
<polygon class="st103" points="77.5,68.7 59.2,18.4 59.2,17.3 77.9,68.7 "/>
<polygon class="st104" points="77.1,68.7 59.2,19.6 59.2,18.4 77.5,68.7 "/>
<polygon class="st105" points="76.6,68.7 59.2,20.7 59.2,19.6 77.1,68.7 "/>
<polygon class="st106" points="76.2,68.7 59.2,21.9 59.2,20.7 76.6,68.7 "/>
<polygon class="st107" points="75.8,68.7 59.2,23 59.2,21.9 76.2,68.7 "/>
<polygon class="st108" points="75.4,68.7 59.2,24.1 59.2,23 75.8,68.7 "/>
<polygon class="st109" points="75,68.7 59.2,25.3 59.2,24.1 75.4,68.7 "/>
<polygon class="st110" points="74.6,68.7 59.2,26.4 59.2,25.3 75,68.7 "/>
<polygon class="st111" points="74.1,68.7 59.2,27.6 59.2,26.4 74.6,68.7 "/>
<polygon class="st111" points="59.2,27.6 74.1,68.7 59.2,68.7 "/>
</g>
</g>
</g>
<g>
<g>
<g>
<defs>
<polygon id="SVGID_5_" points="39,75.5 83,68.7 59.2,56 15.2,62.9 "/>
</defs>
<clipPath id="SVGID_6_">
<use xlink:href="#SVGID_5_" style="overflow:visible;"/>
</clipPath>
<path class="st112" d="M83,75.5v-6.9V75.5z"/>
<rect x="82.1" y="56" class="st112" width="0.9" height="19.5"/>
<rect x="81.2" y="56" class="st113" width="0.9" height="19.5"/>
<rect x="80.3" y="56" class="st114" width="0.9" height="19.5"/>
<rect x="79.4" y="56" class="st115" width="0.9" height="19.5"/>
<rect x="78.5" y="56" class="st116" width="0.9" height="19.5"/>
<rect x="77.6" y="56" class="st117" width="0.9" height="19.5"/>
<rect x="76.7" y="56" class="st118" width="0.9" height="19.5"/>
<rect x="75.8" y="56" class="st119" width="0.9" height="19.5"/>
<rect x="74.9" y="56" class="st120" width="0.9" height="19.5"/>
<rect x="74" y="56" class="st121" width="0.9" height="19.5"/>
<rect x="73.1" y="56" class="st122" width="0.9" height="19.5"/>
<rect x="72.2" y="56" class="st123" width="0.9" height="19.5"/>
<rect x="71.3" y="56" class="st124" width="0.9" height="19.5"/>
<rect x="70.4" y="56" class="st125" width="0.9" height="19.5"/>
<rect x="69.5" y="56" class="st126" width="0.9" height="19.5"/>
<rect x="68.6" y="56" class="st127" width="0.9" height="19.5"/>
<rect x="67.7" y="56" class="st128" width="0.9" height="19.5"/>
<rect x="66.8" y="56" class="st129" width="0.9" height="19.5"/>
<rect x="65.9" y="56" class="st130" width="0.9" height="19.5"/>
<rect x="65" y="56" class="st131" width="0.9" height="19.5"/>
<rect x="64.1" y="56" class="st132" width="0.9" height="19.5"/>
<rect x="63.2" y="56" class="st133" width="0.9" height="19.5"/>
<rect x="62.3" y="56" class="st134" width="0.9" height="19.5"/>
<rect x="61.4" y="56" class="st135" width="0.9" height="19.5"/>
<rect x="60.5" y="56" class="st136" width="0.9" height="19.5"/>
<rect x="59.6" y="56" class="st137" width="0.9" height="19.5"/>
<rect x="58.7" y="56" class="st138" width="0.9" height="19.5"/>
<rect x="57.8" y="56" class="st139" width="0.9" height="19.5"/>
<rect x="56.9" y="56" class="st140" width="0.9" height="19.5"/>
<rect x="56" y="56" class="st141" width="0.9" height="19.5"/>
<rect x="55.1" y="56" class="st142" width="0.9" height="19.5"/>
<rect x="54.2" y="56" class="st143" width="0.9" height="19.5"/>
<rect x="53.3" y="56" class="st144" width="0.9" height="19.5"/>
<rect x="52.4" y="56" class="st145" width="0.9" height="19.5"/>
<rect x="51.5" y="56" class="st146" width="0.9" height="19.5"/>
<rect x="50.6" y="56" class="st147" width="0.9" height="19.5"/>
<rect x="49.7" y="56" class="st148" width="0.9" height="19.5"/>
<rect x="48.8" y="56" class="st149" width="0.9" height="19.5"/>
<rect x="47.9" y="56" class="st150" width="0.9" height="19.5"/>
<rect x="47" y="56" class="st151" width="0.9" height="19.5"/>
<rect x="46.1" y="56" class="st152" width="0.9" height="19.5"/>
<rect x="45.2" y="56" class="st153" width="0.9" height="19.5"/>
<rect x="44.3" y="56" class="st154" width="0.9" height="19.5"/>
<rect x="43.4" y="56" class="st155" width="0.9" height="19.5"/>
<rect x="42.5" y="56" class="st156" width="0.9" height="19.5"/>
<rect x="41.6" y="56" class="st157" width="0.9" height="19.5"/>
<rect x="40.7" y="56" class="st158" width="0.9" height="19.5"/>
<rect x="39.8" y="56" class="st159" width="0.9" height="19.5"/>
<rect x="38.9" y="56" class="st160" width="0.9" height="19.5"/>
<rect x="38" y="56" class="st161" width="0.9" height="19.5"/>
<rect x="37.1" y="56" class="st162" width="0.9" height="19.5"/>
<rect x="15.2" y="56" class="st162" width="21.9" height="19.5"/>
</g>
</g>
</g>
</g>
<g class="st163">
<g>
<g>
<defs>
<polygon id="SVGID_7_" points="39,27.6 15.2,15 15.2,62.9 39,75.5 "/>
</defs>
<clipPath id="SVGID_8_">
<use xlink:href="#SVGID_7_" style="overflow:visible;"/>
</clipPath>
<polygon class="st164" points="37.7,75.5 39,73.7 39,75.5 "/>
<polygon class="st165" points="36.4,75.5 39,71.8 39,73.7 37.7,75.5 "/>
<polygon class="st166" points="35.1,75.5 39,70 39,71.8 36.4,75.5 "/>
<polygon class="st167" points="33.8,75.5 39,68.1 39,70 35.1,75.5 "/>
<polygon class="st168" points="32.5,75.5 39,66.2 39,68.1 33.8,75.5 "/>
<polygon class="st169" points="31.9,75.5 31.4,75.2 39,64.4 39,66.2 32.5,75.5 "/>
<polygon class="st170" points="31.4,75.2 30.5,74.6 39,62.5 39,64.4 "/>
<polygon class="st171" points="30.5,74.6 29.7,74 39,60.7 39,62.5 "/>
<polygon class="st172" points="29.7,74 28.8,73.4 39,58.8 39,60.7 "/>
<polygon class="st173" points="28.8,73.4 27.9,72.8 39,57 39,58.8 "/>
<polygon class="st174" points="27.9,72.8 27.1,72.1 39,55.1 39,57 "/>
<polygon class="st175" points="27.1,72.1 26.2,71.5 39,53.3 39,55.1 "/>
<polygon class="st176" points="26.2,71.5 25.3,70.9 39,51.4 39,53.3 "/>
<polygon class="st177" points="25.3,70.9 24.4,70.3 39,49.6 39,51.4 "/>
<polygon class="st178" points="24.4,70.3 23.6,69.7 39,47.7 39,49.6 "/>
<polygon class="st179" points="23.6,69.7 22.7,69.1 39,45.9 39,47.7 "/>
<polygon class="st180" points="22.7,69.1 21.8,68.5 39,44 39,45.9 "/>
<polygon class="st181" points="21.8,68.5 21,67.9 39,42.2 39,44 "/>
<polygon class="st182" points="21,67.9 20.1,67.3 39,40.3 39,42.2 "/>
<polygon class="st183" points="20.1,67.3 19.2,66.7 39,38.4 39,40.3 "/>
<polygon class="st184" points="19.2,66.7 18.4,66 39,36.6 39,38.4 "/>
<polygon class="st185" points="18.4,66 17.5,65.4 39,34.7 39,36.6 "/>
<polygon class="st186" points="17.5,65.4 16.6,64.8 39,32.9 39,34.7 "/>
<polygon class="st187" points="16.6,64.8 15.7,64.2 39,31 39,32.9 "/>
<polygon class="st188" points="15.7,64.2 15.2,63.8 15.2,63.2 39,29.2 39,31 "/>
<polygon class="st189" points="15.2,61.3 39,27.3 39,29.2 15.2,63.2 "/>
<polygon class="st190" points="15.2,59.5 38.4,26.3 39,26.7 39,27.3 15.2,61.3 "/>
<polygon class="st191" points="15.2,57.6 37.5,25.7 38.4,26.3 15.2,59.5 "/>
<polygon class="st192" points="15.2,55.7 36.7,25 37.5,25.7 15.2,57.6 "/>
<polygon class="st193" points="15.2,53.9 35.8,24.4 36.7,25 15.2,55.7 "/>
<polygon class="st194" points="15.2,52 34.9,23.8 35.8,24.4 15.2,53.9 "/>
<polygon class="st195" points="15.2,50.2 34.1,23.2 34.9,23.8 15.2,52 "/>
<polygon class="st196" points="15.2,48.3 33.2,22.6 34.1,23.2 15.2,50.2 "/>
<polygon class="st197" points="15.2,46.5 32.3,22 33.2,22.6 15.2,48.3 "/>
<polygon class="st198" points="15.2,44.6 31.5,21.4 32.3,22 15.2,46.5 "/>
<polygon class="st199" points="15.2,42.8 30.6,20.8 31.5,21.4 15.2,44.6 "/>
<polygon class="st200" points="15.2,40.9 29.7,20.2 30.6,20.8 15.2,42.8 "/>
<polygon class="st201" points="15.2,39.1 28.8,19.6 29.7,20.2 15.2,40.9 "/>
<polygon class="st202" points="15.2,37.2 28,18.9 28.8,19.6 15.2,39.1 "/>
<polygon class="st203" points="15.2,35.4 27.1,18.3 28,18.9 15.2,37.2 "/>
<polygon class="st204" points="15.2,33.5 26.2,17.7 27.1,18.3 15.2,35.4 "/>
<polygon class="st205" points="15.2,31.6 25.4,17.1 26.2,17.7 15.2,33.5 "/>
<polygon class="st206" points="15.2,29.8 24.5,16.5 25.4,17.1 15.2,31.6 "/>
<polygon class="st207" points="15.2,27.9 23.6,15.9 24.5,16.5 15.2,29.8 "/>
<polygon class="st208" points="15.2,26.1 22.7,15.3 23.6,15.9 15.2,27.9 "/>
<polygon class="st209" points="15.2,24.2 21.7,15 22.3,15 22.7,15.3 15.2,26.1 "/>
<polygon class="st210" points="15.2,22.4 20.4,15 21.7,15 15.2,24.2 "/>
<polygon class="st211" points="15.2,20.5 19.1,15 20.4,15 15.2,22.4 "/>
<polygon class="st212" points="15.2,18.7 17.8,15 19.1,15 15.2,20.5 "/>
<polygon class="st213" points="15.2,16.8 16.5,15 17.8,15 15.2,18.7 "/>
<polygon class="st214" points="15.2,15 15.2,15 16.5,15 15.2,16.8 "/>
<polygon class="st214" points="15.2,15 15.2,15 15.2,15 "/>
</g>
</g>
</g>
<g class="st215">
<g>
<g>
<defs>
<polygon id="SVGID_9_" points="83,20.8 59.2,8.1 15.2,15 39,27.6 "/>
</defs>
<clipPath id="SVGID_10_">
<use xlink:href="#SVGID_9_" style="overflow:visible;"/>
</clipPath>
<path class="st216" d="M83,27.6v-6.9V27.6z"/>
<rect x="81.7" y="8.1" class="st216" width="1.3" height="19.5"/>
<rect x="80.4" y="8.1" class="st217" width="1.3" height="19.5"/>
<rect x="79.2" y="8.1" class="st218" width="1.3" height="19.5"/>
<rect x="77.9" y="8.1" class="st219" width="1.3" height="19.5"/>
<rect x="76.7" y="8.1" class="st220" width="1.3" height="19.5"/>
<rect x="75.4" y="8.1" class="st221" width="1.3" height="19.5"/>
<rect x="74.2" y="8.1" class="st222" width="1.3" height="19.5"/>
<rect x="72.9" y="8.1" class="st223" width="1.3" height="19.5"/>
<rect x="71.7" y="8.1" class="st224" width="1.3" height="19.5"/>
<rect x="70.4" y="8.1" class="st225" width="1.3" height="19.5"/>
<rect x="69.2" y="8.1" class="st226" width="1.3" height="19.5"/>
<rect x="67.9" y="8.1" class="st227" width="1.3" height="19.5"/>
<rect x="66.6" y="8.1" class="st228" width="1.3" height="19.5"/>
<rect x="65.4" y="8.1" class="st229" width="1.3" height="19.5"/>
<rect x="64.1" y="8.1" class="st230" width="1.3" height="19.5"/>
<rect x="62.9" y="8.1" class="st231" width="1.3" height="19.5"/>
<rect x="61.6" y="8.1" class="st232" width="1.3" height="19.5"/>
<rect x="60.4" y="8.1" class="st233" width="1.3" height="19.5"/>
<rect x="59.1" y="8.1" class="st234" width="1.3" height="19.5"/>
<rect x="57.9" y="8.1" class="st235" width="1.3" height="19.5"/>
<rect x="56.6" y="8.1" class="st236" width="1.3" height="19.5"/>
<rect x="55.3" y="8.1" class="st237" width="1.3" height="19.5"/>
<rect x="54.1" y="8.1" class="st238" width="1.3" height="19.5"/>
<rect x="52.8" y="8.1" class="st239" width="1.3" height="19.5"/>
<rect x="51.6" y="8.1" class="st240" width="1.3" height="19.5"/>
<rect x="50.3" y="8.1" class="st241" width="1.3" height="19.5"/>
<rect x="49.1" y="8.1" class="st242" width="1.3" height="19.5"/>
<rect x="47.8" y="8.1" class="st243" width="1.3" height="19.5"/>
<rect x="46.6" y="8.1" class="st244" width="1.3" height="19.5"/>
<rect x="45.3" y="8.1" class="st245" width="1.3" height="19.5"/>
<rect x="44.1" y="8.1" class="st246" width="1.3" height="19.5"/>
<rect x="42.8" y="8.1" class="st247" width="1.3" height="19.5"/>
<rect x="41.5" y="8.1" class="st248" width="1.3" height="19.5"/>
<rect x="40.3" y="8.1" class="st249" width="1.3" height="19.5"/>
<rect x="39" y="8.1" class="st250" width="1.3" height="19.5"/>
<rect x="37.8" y="8.1" class="st251" width="1.3" height="19.5"/>
<rect x="36.5" y="8.1" class="st252" width="1.3" height="19.5"/>
<rect x="35.3" y="8.1" class="st253" width="1.3" height="19.5"/>
<rect x="34" y="8.1" class="st254" width="1.3" height="19.5"/>
<rect x="32.8" y="8.1" class="st255" width="1.3" height="19.5"/>
<rect x="31.5" y="8.1" class="st256" width="1.3" height="19.5"/>
<rect x="30.2" y="8.1" class="st257" width="1.3" height="19.5"/>
<rect x="29" y="8.1" class="st258" width="1.3" height="19.5"/>
<rect x="27.7" y="8.1" class="st259" width="1.3" height="19.5"/>
<rect x="26.5" y="8.1" class="st260" width="1.3" height="19.5"/>
<rect x="25.2" y="8.1" class="st261" width="1.3" height="19.5"/>
<rect x="24" y="8.1" class="st262" width="1.3" height="19.5"/>
<rect x="22.7" y="8.1" class="st263" width="1.3" height="19.5"/>
<rect x="21.5" y="8.1" class="st264" width="1.3" height="19.5"/>
<rect x="20.2" y="8.1" class="st265" width="1.3" height="19.5"/>
<rect x="18.9" y="8.1" class="st266" width="1.3" height="19.5"/>
<rect x="17.7" y="8.1" class="st267" width="1.3" height="19.5"/>
<rect x="16.4" y="8.1" class="st268" width="1.3" height="19.5"/>
<polygon class="st269" points="15.2,15 15.2,8.1 16.4,8.1 16.4,27.6 15.2,27.6 "/>
<path class="st269" d="M15.2,8.1V15V8.1z"/>
</g>
</g>
</g>
<g class="st215">
<g>
<g>
<defs>
<polygon id="SVGID_11_" points="39,27.6 39,75.5 83,68.7 83,20.8 "/>
</defs>
<clipPath id="SVGID_12_">
<use xlink:href="#SVGID_11_" style="overflow:visible;"/>
</clipPath>
<polygon class="st270" points="77.2,75.5 83,68.7 83,75.5 "/>
<polygon class="st270" points="76.2,75.5 83,67.5 83,68.7 77.2,75.5 "/>
<polygon class="st271" points="75.3,75.5 83,66.3 83,67.5 76.2,75.5 "/>
<polygon class="st272" points="74.3,75.5 83,65.1 83,66.3 75.3,75.5 "/>
<polygon class="st273" points="73.3,75.5 83,64 83,65.1 74.3,75.5 "/>
<polygon class="st274" points="72.3,75.5 83,62.8 83,64 73.3,75.5 "/>
<polygon class="st275" points="71.3,75.5 83,61.6 83,62.8 72.3,75.5 "/>
<polygon class="st276" points="70.3,75.5 83,60.4 83,61.6 71.3,75.5 "/>
<polygon class="st277" points="69.3,75.5 83,59.3 83,60.4 70.3,75.5 "/>
<polygon class="st278" points="68.4,75.5 83,58.1 83,59.3 69.3,75.5 "/>
<polygon class="st279" points="67.4,75.5 83,56.9 83,58.1 68.4,75.5 "/>
<polygon class="st280" points="66.4,75.5 83,55.7 83,56.9 67.4,75.5 "/>
<polygon class="st281" points="65.4,75.5 83,54.6 83,55.7 66.4,75.5 "/>
<polygon class="st282" points="64.4,75.5 83,53.4 83,54.6 65.4,75.5 "/>
<polygon class="st283" points="63.4,75.5 83,52.2 83,53.4 64.4,75.5 "/>
<polygon class="st284" points="62.4,75.5 83,51 83,52.2 63.4,75.5 "/>
<polygon class="st285" points="61.5,75.5 83,49.9 83,51 62.4,75.5 "/>
<polygon class="st286" points="60.5,75.5 83,48.7 83,49.9 61.5,75.5 "/>
<polygon class="st287" points="59.5,75.5 83,47.5 83,48.7 60.5,75.5 "/>
<polygon class="st288" points="58.5,75.5 83,46.3 83,47.5 59.5,75.5 "/>
<polygon class="st289" points="57.5,75.5 83,45.2 83,46.3 58.5,75.5 "/>
<polygon class="st290" points="56.5,75.5 83,44 83,45.2 57.5,75.5 "/>
<polygon class="st291" points="55.6,75.5 83,42.8 83,44 56.5,75.5 "/>
<polygon class="st292" points="54.6,75.5 83,41.7 83,42.8 55.6,75.5 "/>
<polygon class="st293" points="53.6,75.5 83,40.5 83,41.7 54.6,75.5 "/>
<polygon class="st294" points="52.6,75.5 83,39.3 83,40.5 53.6,75.5 "/>
<polygon class="st295" points="51.6,75.5 83,38.1 83,39.3 52.6,75.5 "/>
<polygon class="st296" points="50.6,75.5 83,37 83,38.1 51.6,75.5 "/>
<polygon class="st297" points="49.6,75.5 83,35.8 83,37 50.6,75.5 "/>
<polygon class="st298" points="48.7,75.5 83,34.6 83,35.8 49.6,75.5 "/>
<polygon class="st299" points="47.7,75.5 83,33.4 83,34.6 48.7,75.5 "/>
<polygon class="st300" points="46.7,75.5 83,32.3 83,33.4 47.7,75.5 "/>
<polygon class="st301" points="45.7,75.5 83,31.1 83,32.3 46.7,75.5 "/>
<polygon class="st302" points="44.7,75.5 83,29.9 83,31.1 45.7,75.5 "/>
<polygon class="st303" points="43.7,75.5 83,28.7 83,29.9 44.7,75.5 "/>
<polygon class="st304" points="42.8,75.5 83,27.6 83,28.7 43.7,75.5 "/>
<polygon class="st305" points="41.8,75.5 83,26.4 83,27.6 42.8,75.5 "/>
<polygon class="st306" points="40.8,75.5 83,25.2 83,26.4 41.8,75.5 "/>
<polygon class="st307" points="39.8,75.5 83,24 83,25.2 40.8,75.5 "/>
<polygon class="st308" points="39,75.4 83,22.9 83,24 39.8,75.5 39,75.5 "/>
<polygon class="st309" points="39,74.2 83,21.7 83,22.9 39,75.4 "/>
<polygon class="st310" points="39,73 82.8,20.8 83,20.8 83,21.7 39,74.2 "/>
<polygon class="st311" points="39,71.8 81.8,20.8 82.8,20.8 39,73 "/>
<polygon class="st312" points="39,70.7 80.8,20.8 81.8,20.8 39,71.8 "/>
<polygon class="st313" points="39,69.5 79.8,20.8 80.8,20.8 39,70.7 "/>
<polygon class="st314" points="39,68.3 78.9,20.8 79.8,20.8 39,69.5 "/>
<polygon class="st315" points="39,67.1 77.9,20.8 78.9,20.8 39,68.3 "/>
<polygon class="st316" points="39,66 76.9,20.8 77.9,20.8 39,67.1 "/>
<polygon class="st317" points="39,64.8 75.9,20.8 76.9,20.8 39,66 "/>
<polygon class="st317" points="75.9,20.8 39,64.8 39,20.8 "/>
</g>
</g>
</g>
<g>
<g>
<g>
<path class="st0" d="M36.6,28.1c0-1.4,1.1-2.7,2.5-2.9l22.5-3.6v31.9c0,2.6-1.9,4.7-4.4,5.1L36.5,62L36.6,28.1z M56.7,27.3
l-15.3,2.4l0,26.6l15.4-2.5V27.3z"/>
</g>
</g>
</g>
<g>
<g>
<rect x="36.6" y="48.3" class="st0" width="4.8" height="28.2"/>
</g>
</g>
<g>
<g>
<rect x="59.2" y="3.9" class="st0" width="2.4" height="36.7"/>
</g>
</g>
<g>
<g>
<path class="st0" d="M83,23.4v42.4c0,1.5-1.1,2.9-2.6,3.1l-35.3,5.7c-1.9,0.3-3.6-1.2-3.6-3.1v-7.6c0-1.5,1.1-2.9,2.6-3.1l14-2.2
c2-0.3,3.5-2.1,3.5-4.1l0.2-39.7c0-2.4,2.5-3.9,4.6-2.8l14.3,7.7C82.1,20.4,83,21.8,83,23.4z M86.5,16L62.3,3
c-0.8-0.6-1.9-0.1-1.9,0.9l-1.1,51c0,0.6-0.4,1.1-1,1.2L40,59c-0.6,0.1-1,0.6-1,1.2l0,18.7c0,0.7,0.7,1.3,1.4,1.2L86,71.8
c0.6-0.1,1-0.6,1-1.2V17C87,16.6,86.8,16.3,86.5,16z"/>
</g>
</g>
<g>
<g>
<path class="st0" d="M59.2,11.7l0,7.5c0,1.6-1.2,3-2.7,3.2l-17.1,2.8c-1.6,0.3-2.7,1.6-2.7,3.2l0,40.3c0,2.5-2.6,4-4.8,2.9
l-14.8-7.9c-1.1-0.6-1.7-1.7-1.7-2.9V17.6c0-1.6,1.2-3,2.8-3.2l37.5-5.8C57.4,8.2,59.2,9.7,59.2,11.7z M58.8,2.6l-45.5,7.9
c-0.8,0.1-1.4,0.8-1.4,1.6l-0.2,49.6c-0.1,0.7,0.3,1.3,0.9,1.6l25.2,15.4c1.2,0.6,2.6-0.2,2.5-1.6L39,29.3c0-0.9,0.6-1.7,1.5-1.8
l18.5-2.1c0.9-0.1,1.5-0.8,1.5-1.7l0.2-19.4C60.8,3.3,59.8,2.4,58.8,2.6z"/>
</g>
</g>
<g>
<path class="st0" d="M47.5,43.8c-0.6,0-1.1-0.4-1.2-1c-0.1-0.7,0.3-1.3,1-1.4l10.1-1.6c0.7-0.1,1.3,0.3,1.4,1
c0.1,0.7-0.3,1.3-1,1.4l-10.1,1.6C47.6,43.8,47.6,43.8,47.5,43.8z"/>
</g>
<g>
<g>
<path class="st0" d="M55.2,42.6L55.2,42.6c0.8-0.1,1.6,0.5,1.6,1.3v8.7l4.5,0V30.1l-4.5,0.2V38c0,1.1-0.8,2-1.8,2.2l-1.7,0.3
L55.2,42.6z"/>
</g>
</g>
</g>
<g>
<g>
<polygon class="st318" points="112.5,28.8 112.5,37.1 125.6,37.1 125.6,41.2 112.5,41.2 112.5,49.5 125.6,49.5 125.6,53.6
108,53.6 108,24.7 125.6,24.7 125.6,28.8 "/>
</g>
<g>
<path class="st318" d="M147.2,38.3v14.2l-3.8,1.8h-0.6v-16c0-1.7-0.8-1.9-1.5-1.9h-4.3c-0.7,0-1.5,0.2-1.5,1.9v14.2l-3.8,1.8H131
v-16c0-4,2-6,6-6h4.3C145.2,32.3,147.2,34.3,147.2,38.3z"/>
</g>
<g>
<polygon class="st318" points="161.5,32.3 161.5,32.9 159.8,36.4 156.7,36.4 156.7,52.5 152.8,54.3 152.2,54.3 152.2,27.7
156,25.9 156.7,25.9 156.7,32.3 "/>
</g>
<g>
<path class="st318" d="M174.6,32.3h-4.3c-4,0-6,2-6,6v9.3c0,4,2,6,6,6h4.3c0.6,0,1.1-0.1,1.5-0.3v2c0,1.7-0.8,1.9-1.5,1.9h-10.3
v4.1h10.3c4,0,6-2,6-6v-17C180.5,34.3,178.5,32.3,174.6,32.3z M176.1,47.6c0,1.7-0.8,1.9-1.5,1.9h-4.3c-0.7,0-1.5-0.2-1.5-1.9
v-9.3c0-1.7,0.8-1.9,1.5-1.9h4.3c0.7,0,1.5,0.2,1.5,1.9V47.6z"/>
<path class="st318" d="M197,32.3v0.6l-1.6,3.5H192c-0.7,0-1.5,0.2-1.5,1.9v14.2l-3.8,1.8H186v-16c0-4,2-6,6-6H197z"/>
</g>
<g>
<path class="st318" d="M208,32.3h-7v4.1h7c0.7,0,1.5,0.2,1.5,1.9v1.1c-0.5-0.2-1-0.3-1.5-0.3h-3.9c-4,0-6,2-6,6v2.6c0,4,2,6,6,6
h3.9c0.6,0,1.1-0.1,1.5-0.3v1.4h0.6l3.8-1.8V38.3C214,34.3,212,32.3,208,32.3z M208,49.5h-3.9c-0.7,0-1.5-0.2-1.5-1.9V45
c0-1.7,0.8-1.9,1.5-1.9h3.9c0.7,0,1.5,0.2,1.5,1.9v2.6C209.5,49.3,208.7,49.5,208,49.5z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 44 KiB

@ -0,0 +1,67 @@
{
"Title" : "Title",
"Description" : "Description",
"ShortDescription" : "Short Description",
"Category" : "Category",
"Visibility" : "Visibility",
"Devices" : "Devices",
"Roles" : "Roles",
"Groups" : "Groups",
"Tags" : "Tags",
"Platform" : "Platform",
"Platforms" : "Platforms",
"Applications": "Applications",
"No.Platform" : "No Platforms",
"Screenshots" : "Screenshots",
"Icon" : "Icon",
"Info" : "Info",
"Banner" : "Banner",
"Create.Application" : "Create Application",
"Back" : "Back",
"Cancel" : "Cancel",
"Finish" : "Finish",
"Continue" : "Continue",
"Name" : "Name",
"Application.Name" : "Application Name",
"General" : "General",
"App.Releases" : "Application Releases",
"Package.Manager" : "Package Manager",
"Save" : "Save",
"Create.Release" : "Create Release",
"Release.Channel" : "Release Channel",
"Release" : "Release",
"New.Release.For" : "New Release for",
"Upload.Package.File" : "Upload Package File",
"Upload" : "Upload",
"Select.from.package.library" : "Select from package library",
"Release.Name" : "Release Name",
"Release.Notes" : "Release Notes",
"Send.for.Review" : "Send for Review",
"Production.Releases" : "Production Releases",
"Beta.Releases" : "Beta Releases",
"Alpha.Releases" : "Alpha Releases",
"Version" : "Version",
"Status" : "Status",
"App.Publisher" : "Application Publisher",
"Search.Apps" : "Search for Applications",
"View.In.Store" : "View in Store",
"Last.Updated" : "Last updated on",
"Installs" : "Installs",
"General.Info" : "General Info",
"Select.Platform": "Select Platform",
"Add.Release" : "Add Release to Application",
"Share.With.Tenants" : "Share with Tenants",
"Disable" : "Disable",
"File.Based" : "File Based",
"Activate" : "Activate",
"Yes" : "Yes",
"No" : "No",
"No.Platform.Tags" : "No Platform Tags",
"Create.Platform" : "Create Platform",
"Optional": "Optional",
"Identifier": "Identifier",
"Next": "Next",
"Platform.Enable": "Enable Platform",
"Share.with.Tenants": "Share between all tenants",
"Platform.Properties": "Platform Properties"
}

@ -0,0 +1,903 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.
*/
@font-face {
font-family: "Roboto-Medium";
src: url('../../fonts/Roboto-Medium.woff');
src: local("Roboto-Medium"), url("../../fonts/Roboto-Medium.ttf") format("ttf");
src: local("Roboto-Medium"), url("../../fonts/Roboto-Medium.woff") format("woff");
src: local("Roboto-Medium"), url("../../fonts/Roboto-Medium.woff2") format("woff2");
}
@font-face {
font-family: "Roboto-Regular";
src: url("../../fonts/Roboto-Regular.woff");
src: local("Roboto-Regular"), url("../../fonts/Roboto-Regular.ttf") format("ttf");
src: local("Roboto-Regular"), url("../../fonts/Roboto-Regular.woff") format("woff");
src: local("Roboto-Regular"), url("../../fonts/Roboto-Regular.woff2") format("woff2");
}
/*Colors*/
.primary {
color: white;
background-color: #2196f3 !important;
}
.primary-flat {
color: #2196F3 !important;
}
.danger {
color: white;
background-color: #e91e63 !important;
}
.danger-flat {
color: #e91e63 !important;
}
.grey {
color: #b3b3b3 !important;
}
/* ==================================================================== */
/* Custom button styles based on material design specs. */
.custom-raised {
font-family: Roboto-Medium;
text-transform: uppercase !important;
font-size: 14px !important;
padding-left: 16px !important;
border-radius: 2px !important;
padding-right: 16px !important;
height: 36px !important;
border: none !important;
}
.custom-raised:hover {
cursor: pointer;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 0 1px rgba(0, 0, 0, 0.08) !important;
-webkit-box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 0 1px rgba(0, 0, 0, 0.08) !important;
background-color: #1976D2 !important;
}
.custom-raised:focus {
box-shadow: none !important;
-webkit-box-shadow: none !important;
background-color: #1976D2 !important;
}
.custom-flat {
font-family: Roboto-Medium;
height: 36px !important;
border-radius: 2px !important;
margin-left: 8px !important;
margin-right: 8px !important;
padding-left: 8px !important;
padding-right: 8px !important;
background-color: transparent !important;
text-transform: uppercase;
outline: none !important;
border: none !important;
}
.custom-flat:hover {
cursor: pointer;
background-color: rgba(0, 0, 0, 0.12) !important;
}
.custom-flat:focus {
outline: none !important;
border: none !important;
-webkit-box-shadow: none !important;
box-shadow: none !important;
background-color: rgba(0, 0, 0, 0.40) !important;
}
.circle-button {
border-radius: 100% !important;
height: 36px !important;
width: 36px;
}
/* ==================================================================== */
/* Body Styling */
body {
width: 100%;
font-family: "Roboto-Regular" !important;
font-size: 14px !important;
background-color: #e8e8e8 !important;
}
.app-manager-title {
font-family: "Roboto-Medium";
font-size: 20px;
}
.app-manager-sub-title {
font-family: "Roboto-Regular";
font-size: 18px;
}
#app-mgt-footer {
clear: both;
position: relative;
height: 50px;
width: 100%;
color: white;
background-color: #334d88;
}
/* Login page styles*/
#userName {
border-radius: 0;
}
#password {
border-radius: 0;
}
.login-btn {
float: right;
}
.login-header {
background-color: #3f50b5;
color: white;
height: 128px;
width: 100%;
margin: 0 !important;
padding: 20px;
box-shadow: -2px 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
#login-card {
width: 25%;
height: 50%;
margin: 10% auto;
font-family: Roboto-Regular;
font-size: 14px;
border-radius: 0;
background-color: #ffffff;
box-shadow: -2px 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
.login-header-title {
font-family: Roboto-Medium;
font-size: 20px;
font-weight: 500;
}
.login-header-logo {
height: 70px;
width: 150px;
}
.login-form {
margin: 0 !important;
padding: 40px;
}
/* Base layout container */
/* Base layout header content*/
.header-content {
height: 128px !important;
width: 100% !important;
margin: 0 10px 0 0;
background-color: #3f50b5 !important;
position: fixed; /* Set the navbar to fixed position */
top: 0; /* Position the navbar at the top of the page */
z-index: 2;
box-shadow: -2px 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
/* Contains the header styles.*/
.header {
padding: 24px 24px 10px 24px;
/*margin: 16px 16px 20px 16px;*/
position: relative;
}
#header-text {
color: #ffffff;
font-size: 20px;
font-family: Roboto-Medium;
top: 10px;
margin-left: 10px;
}
/* The buttons in the header (User and Notification)*/
.header-button-container {
display: flex;
justify-content: flex-end;
}
.header-user-name {
font-family: Roboto-Medium;
font-size: 14px;
padding-top: 15px;
color: white;
}
.header-image {
height: 43px;
width: 100px;
margin-right: 24px;
}
#header-button {
border-radius: 50%;
background-color: transparent;
border: none;
height: 50px;
width: 50px;
margin-right: 10px;
position: relative;
outline: none;
}
#header-button:hover {
background-color: #4353bd;
cursor: pointer;
}
#header-button i {
position: absolute;
bottom: 19px;
left: 17px;
}
.btn-header {
margin-top: 15px;
margin-right: 20px;
color: white;
}
#sub-title {
font-family: Roboto-Regular;
font-size: 18px;
font-weight: 600;
padding-top: 5px;
padding-left: 18px;
color: RGBA(0, 0, 0, 1);
}
/* Search box styles */
.search-box {
display: flex;
float: right;
}
.search-box i {
position: absolute;
top: 5px;
color: #BaBaBa;
}
#search {
position: relative;
color: white;
background-color: transparent;
left: 15px;
top: 0px;
height: 25px;
outline: none;
border: none;
border-radius: 0%;
}
/* Application Add button */
#add-btn-container {
position: absolute;
top: 98px;
}
.add-btn {
background-color: #ff5722;
}
.add-btn:hover {
background-color: #E64A19;
}
#sub-title-container {
height: 100px;
padding: 50px 0 20px 0;
}
.application-container {
padding: 0 !important;
min-height: 100% !important;
margin-top: 128px !important;
}
/* Holds the app pages. */
.store-card {
height: auto;
background-color: white;
box-shadow: 2px 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
padding: 24px;
}
.platform-link-placeholder {
color: #888888;
float: right;
padding-bottom: 10px;
}
.platform-link-placeholder i {
margin-right: 4px;
}
.application-list {
transition: margin-right .5s;
}
#batch-content {
display: flex;
margin-top: 5px;
}
.app-list-icon {
border-radius: 50%;
height: 50px;
width: 50px
}
.app-table-row {
height: 62px;
cursor: pointer;
padding-top: 6px;
font-family: "Roboto-Regular";
font-size: medium;
}
.app-table-row:hover {
color: white;
background-color: #3f50b5;
}
.app-list-table-header {
margin-top: 30px;
margin-bottom: 10px;
font-family: "Roboto-Medium";
font-size: 15px;
}
.app-view-image {
height: 100px;
width: 100px;
border-radius: 50%;
}
#app-visibility-default {
display: none;
}
#app-image-screenshot {
width: 300px;
height: 300px;
}
#app-image-icon {
width: 300px;
height: 300px;
}
#app-image-banner {
width: 400px;
height: 300px;
}
#form-error {
color: red;
}
.application-create-banner-dropzone {
width: 300px;
height: 150px;
border-radius: 5%;
position: relative;
border: dashed #888888 2px;
}
.application-create-banner-dropzone i {
position: absolute;
top: 65px;
left: 145px;
}
.application-create-screenshot-dropzone {
width: 150px;
height: 150px;
margin: 0 5px 0 5px;
border-radius: 10%;
position: relative;
border: dashed #888888 2px;
}
.application-create-screenshot-dropzone i {
position: absolute;
top: 65px;
left: 65px;
}
.application-create-icon-dropzone {
width: 150px;
height: 150px;
border-radius: 10%;
position: relative;
border: dashed #888888 2px;
}
.application-create-icon-dropzone i {
position: absolute;
top: 65px;
left: 65px;
}
#screenshot-container {
max-width: 600px;
display: flex;
overflow-x: auto;
height: 200px;
}
#app-icon-container {
height: 300px;
overflow-x: auto;
}
#modal-body-content {
max-height: 700px;
padding-left: 24px;
overflow-y: auto;
}
.custom-footer {
justify-content: inherit !important;
margin: 0 !important;
}
.footer-main-btn {
display: flex;
justify-content: flex-end;
}
#img-btn-screenshot {
margin: 0 5px 0 5px;
}
#app-create-modal {
max-width: 850px;
border-radius: 0% !important;
}
.app-create-modal-header {
background-color: #4353bd;
color: white;
padding: 24px !important;
}
.app-create-modal-content {
padding: 0 !important;
}
#store {
border: none;
border-bottom: solid #BDBDBD 1px;
border-radius: 0px;
width: 200px;
}
#version {
border: none;
border-bottom: solid #BDBDBD 1px;
border-radius: 0px;
width: 200px;
}
#app-release-switch-content {
display: flex;
}
#app-release-switch-label {
position: absolute;
float: left;
}
#app-release-switch {
position: absolute;
right: 10px;
}
.image-sub-title {
font-style: italic;
font-size: 12px;
color: #818181;
}
/* Application View */
#application-view-content {
width: 100%;
}
#application-view-row {
margin: 10px 10px 0 20px;
}
#app-icon {
height: 100px;
width: 100px;
border: solid 1px black;
border-radius: 50%;
}
.app-updated-date {
color: #888888;
}
.app-install-count {
font-style: italic;
}
.app-details-tbl {
outline: none;
border-color: #2196F3;
}
.app-details-tbl tr {
margin: 20px 0 0 0;
}
.app-details-tbl td {
margin-left: 10px;
max-width: 400px;
}
/* Application Edit Base Layout */
#application-edit-header {
height: 40px;
width: 100%;
margin-top: 20px;
margin-bottom: 20px;
font-size: 25px;
}
.application-header-text {
margin: 10px 0px 0px 10px;
}
#save-btn-content {
float: right;
}
#app-save-btn {
border-radius: 0%;
}
.save-btn {
margin: 5px 5px 5px 0px;
height: 70%;
width: 50%;
float: right;
}
.save-btn:hover {
cursor: pointer;
}
/*Tab styling*/
div.tab {
float: left;
border-right: 1px solid #d8d8d8;
height: 100%;
}
/* Style the tab buttons */
div.tab button {
display: block;
background-color: inherit;
color: black;
padding: 15px 16px;
width: 100%;
border: none;
outline: none;
text-align: left;
cursor: pointer;
transition: 0.3s;
}
/* Change background color of buttons on hover */
div.tab button:hover {
background-color: #ddd6d7;
cursor: pointer;
}
/* Create an active/current "tab button" class */
div.tab button.active {
background-color: #1b3bcc;
color: white;
}
#application-edit-main-container {
display: flex;
}
#application-edit-outer-content {
height: auto;
}
#app-edit-content {
height: 100%;
position: relative;
}
.back-to-app {
position: absolute;
height: 50px;
width: 50px;
border-radius: 50%;
}
.back-to-app i {
padding: 12px 10px 10px 12px;
}
.back-to-app:hover {
cursor: pointer;
background-color: #dedede;
transition: .5s;
}
/* Create Release and Release management */
.release-header {
margin-top: 20px;
margin-bottom: 20px;
}
.release-create {
height: 150px;
margin-bottom: 20px;
}
.release-detail-content {
width: 100%;
margin-top: 20%;
height: 300px;
}
.form-btn {
float: right;
margin-bottom: 10px;
}
.release-content {
height: 180px;
width: 95%;
border: dashed 1px #626262;
border-radius: 2%;
position: relative;
background-color: #e8e8e8;
}
.release-content:after {
content: "";
letter-spacing: 4px;
}
.release {
margin: 30px 10px 20px 30px;
}
.no-release-content {
position: absolute;
margin-top: 10px;
left: 40%;
}
.button-add:hover {
cursor: pointer;
}
.release-inner {
margin-top: 5%;
}
/* Application Edit General Info */
.app-edit-general-info {
margin-top: 20px;
max-width: 100%;
}
.save-info {
float: right;
margin-bottom: 10px;
}
.app-view-field {
font-family: Roboto-Medium;
font-size: 14px;
}
.app-view-text {
font-family: Roboto-Regular;
font-size: 14px;
}
/* Platform Specific Styles. */
#platform-listing {
margin: 10px;
}
.create-platform i {
margin-right: 10px;
}
#platform-list {
margin-top: 20px;
display: flex;
flex-flow: wrap;
}
.platform-content {
margin: 10px;
padding-top: 16px;
box-shadow: 2px 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
.platform-content .row {
margin: 0;
}
.platform-content .col {
padding: 0;
}
.platform-content-basic {
padding: 0 16px 0 16px;
display: flex;
}
.platform-content-more-outer {
}
.platform-content-more {
padding: 16px 16px 24px 16px;
}
.platform-content-footer {
display: flex;
padding: 8px 8px 8px 8px;
}
.platform-text-container {
padding: 8px 16px 0 16px;
}
.circle-button {
float: right;
}
.platform-icon-letter {
text-align: center;
text-transform: uppercase;
font-family: Roboto-Medium;
font-size: 70px;
color: white;
padding-top: 15px;
}
.platform-icon-container {
height: 120px;
width: 120px;
background-color: #01579B;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 0 1px rgba(0, 0, 0, 0.08) !important;
-webkit-box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 0 1px rgba(0, 0, 0, 0.08) !important;
}
.platform-property-container {
padding-top: 20px;
font-family: Roboto-Regular;
font-size: 14px;
}
.platform-property-row {
align-items: center;
}
.circle-btn-clear {
background-color: white !important;
color: rgba(0, 0, 0, 0.50) !important;
}
.circle-btn-clear:hover {
background-color: white !important;
color: rgba(0, 0, 0, 0.38) !important;
}
.circle-btn-clear:focus {
background-color: white !important;
color: rgba(0, 0, 0, 0.60) !important;
}
.data-table-row-cell {
padding-top: 14px;
}
.error-code {
text-align: center;
font-family: Roboto-Medium;
font-weight: 800;
font-size: 15em;
color: #BaBaBa;
}
.error-code p {
}
.error-text {
text-align: center;
font-family: Roboto-Regular;
font-size: 14px;
font-weight: 500;
color: #9e9e9e;
}
.circle-btn-add {
background-color: #bababa !important;
border-radius: 50% !important;
height: 30px !important;
width: 30px;
text-align: -webkit-center;
font-size: 18px;
padding: 6px !important;
}
.circle-btn-add:hover {
background-color: #828282 !important;
}
/**
If you need to change the color of active steps in stepper,
uncomment the following and set the background color and font color as needed.
*/
/*
.stepper-active-index {
background-color: #0a6eff !important;
color: white !important;
}
.stepper-passed-index {
background-color: #0a6eff !important;
color: green !important;
}
*/

@ -0,0 +1,67 @@
/*
* 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.
*/
.ant-upload.ant-upload-drag {
height: 170px;
}
.release .release-icon{
margin-right: 15px;
}
.release .release-icon img{
width: 100%;
border-radius: 28%;
}
.release .release-title{
margin-left: 15px;
}
.release .release-screenshot img{
width: 100%;
border-radius: 15px;
padding: 5px;
}
.logo-image {
/*width: 120px;*/
height: 31px;
margin: 0 5px 16px 24px;
float: left;
}
.logo-image img{
height: 35px;
}
.main-container{
background: #f0f2f5;
min-height: 780px
}
.profile{
float:right;
margin-right: 2%;
}
@media only screen and (min-width: 768px) {
.main-container{
padding: 24px;
}
}

@ -0,0 +1,111 @@
/*
* 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 "antd/dist/antd.less";
import RouteWithSubRoutes from "./components/RouteWithSubRoutes";
import {
BrowserRouter as Router,
Redirect, Switch,
} from 'react-router-dom';
import axios from "axios";
import {Layout, Spin, Result} from "antd";
import ConfigContext from "./context/ConfigContext";
const {Content} = Layout;
const loadingView = (
<Layout>
<Content style={{
padding: '0 0',
paddingTop: 300,
backgroundColor: '#fff',
textAlign: 'center'
}}>
<Spin tip="Loading..."/>
</Content>
</Layout>
);
const errorView = (
<Result
style={{
paddingTop: 200
}}
status="500"
title="Error occurred while loading the configuration"
subTitle="Please refresh your browser window"
/>
);
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
error: false,
config: {}
}
}
componentDidMount() {
axios.get(
window.location.origin + "/store/public/conf/config.json",
).then(res => {
console.log(res);
this.setState({
loading: false,
config: res.data
})
}).catch((error) => {
this.setState({
loading: false,
error: true
})
});
}
render() {
const {loading, error} = this.state;
const applicationView = (
<Router>
<ConfigContext.Provider value={this.state.config}>
<div>
<Switch>
<Redirect exact from="/store" to="/store/android"/>
{this.props.routes.map((route) => (
<RouteWithSubRoutes key={route.path} {...route} />
))}
</Switch>
</div>
</ConfigContext.Provider>
</Router>
);
return (
<div>
{loading && loadingView}
{!loading && !error && applicationView}
{error && errorView}
</div>
);
}
}
export default App;

@ -0,0 +1,27 @@
/*
* 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 ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

@ -0,0 +1,37 @@
/*
* 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 {Route} from 'react-router-dom';
class RouteWithSubRoutes extends React.Component{
props;
constructor(props){
super(props);
this.props = props;
}
render() {
return(
<Route path={this.props.path} exact={this.props.exact} render={(props) => (
<this.props.component {...props} {...this.props} routes={this.props.routes}/>
)}/>
);
}
}
export default RouteWithSubRoutes;

@ -0,0 +1,78 @@
/*
* 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 {Card, Typography, Col, Row} from 'antd';
import React from "react";
import {Link} from "react-router-dom";
import "../../App.css";
import StarRatings from 'react-star-ratings';
const {Meta} = Card;
const {Text} = Typography;
class AppCard extends React.Component {
constructor(props) {
super(props);
this.handleReleasesClick = this.handleReleasesClick.bind(this);
}
handleReleasesClick() {
this.props.openReleasesModal(this.props.app);
}
render() {
const app = this.props.app;
const release = this.props.app.applicationReleases[0];
const description = (
<div className="appCard">
<Link to={"/store/"+app.deviceType+"/apps/" + release.uuid}>
<Row className="release">
<Col span={24} className="release-icon">
<img src={release.iconPath} alt="icon"/>
{/*<Avatar shape="square" size={128} src={release.iconPath} />*/}
</Col>
<Col span={24}>
<Text strong level={4}>{app.name}</Text><br/>
<Text type="secondary" level={4}>{app.deviceType}</Text><br/>
<StarRatings
rating={app.rating}
starRatedColor="#777"
starDimension = "12px"
starSpacing = "0"
numberOfStars={5}
name='rating'
/>
</Col>
</Row>
</Link>
</div>
);
return (
<Card style={{marginTop: 16}}>
<Meta
description={description}
/>
</Card>
);
}
}
export default AppCard;

@ -0,0 +1,120 @@
/*
* 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 AppCard from "./AppCard";
import {Col, message, notification, Row, Result, Skeleton} from "antd";
import axios from "axios";
import {withConfigContext} from "../../context/ConfigContext";
class AppList extends React.Component {
constructor(props) {
super(props);
this.state = {
apps: [],
loading: true
}
}
componentDidMount() {
const {deviceType} = this.props;
this.props.changeSelectedMenuItem(deviceType);
this.fetchData(deviceType);
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.deviceType !== this.props.deviceType) {
const {deviceType} = this.props;
this.props.changeSelectedMenuItem(deviceType);
this.fetchData(deviceType);
}
}
fetchData = (deviceType) => {
const config = this.props.context;
const payload = {};
if (deviceType === "web-clip") {
payload.appType = "WEB_CLIP";
} else {
payload.deviceType = deviceType;
}
this.setState({
loading: true
});
//send request to the invoker
axios.post(
window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.store + "/applications/",
payload,
).then(res => {
if (res.status === 200) {
//todo remove this property check after backend improvement
let apps = (res.data.data.hasOwnProperty("applications")) ? res.data.data.applications : [];
this.setState({
apps: apps,
loading: false
})
}
}).catch((error) => {
console.log(error.response);
if (error.hasOwnProperty("response") && error.response.status === 401) {
//todo display a popup with error
message.error('You are not logged in');
window.location.href = window.location.origin+ '/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load apps.",
});
}
this.setState({loading: false});
});
};
render() {
const {apps,loading} = this.state;
return (
<Skeleton loading={loading} active>
<Row gutter={16}>
{apps.length === 0 && (
<Result
status="404"
title="No apps, yet."
subTitle="No apps available, yet! When the administration uploads, apps will show up here."
// extra={<Button type="primary">Back Home</Button>}
/>
)}
{apps.map(app => (
<Col key={app.id} xs={12} sm={6} md={6} lg={4} xl={3}>
<AppCard key={app.id}
app={app}
/>
</Col>
))}
</Row>
</Skeleton>
);
}
}
export default withConfigContext(AppList);

@ -0,0 +1,90 @@
/*
* 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.
*/
.d-rating .numeric-data{
box-sizing: border-box;
display: inline-block;
padding: 20px 0 20px 0;
vertical-align: top;
text-align: center;
width: 30%;
}
.d-rating .bar-containers{
box-sizing: border-box;
display: inline-block;
padding: 20px 20px 20px 30px;
vertical-align: top;
width: 70%;
}
.d-rating .bar-containers .bar-container{
color: #737373;
font-weight: 400;
height: 20px;
margin-bottom: 4px;
position: relative;
width: 100%;
}
.d-rating .bar-containers .bar-container .number{
font-size: 11px;
left: -16px;
letter-spacing: 1px;
position: absolute;
}
.d-rating .bar-containers .bar-container .bar{
transition: width .25s ease;
display: inline-block;
height: 100%;
opacity: .8;
border-radius: 5px;
}
.bar-container .rate-5{
background: #57bb8a;
}
.bar-container .rate-4{
background: #9ace6a;
}
.bar-container .rate-3{
background: #ffcf02;
}
.bar-container .rate-2{
background: #ff9f02;
}
.bar-container .rate-1{
background: #ff6f31;
}
.d-rating .numeric-data .rate{
color: #333;
font-size: 64px;
font-weight: 100;
line-height: 64px;
padding-bottom: 6px;
}
.d-rating .numeric-data .people-count{
padding-top: 6px;
}

@ -0,0 +1,140 @@
/*
* 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 {Row, Typography, Icon} from "antd";
import StarRatings from "react-star-ratings";
import "./DetailedRating.css";
import axios from "axios";
import {withConfigContext} from "../../../context/ConfigContext";
const { Text } = Typography;
class DetailedRating extends React.Component{
constructor(props){
super(props);
this.state={
detailedRating: null
}
}
componentDidMount() {
const {type,uuid} = this.props;
this.getData(type,uuid);
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.uuid !== this.props.uuid) {
const {type,uuid} = this.props;
this.getData(type,uuid);
}
}
getData = (type, uuid)=>{
const config = this.props.context;
return axios.get(
window.location.origin+ config.serverConfig.invoker.uri +config.serverConfig.invoker.store+"/reviews/"+uuid+"/"+type+"-rating",
).then(res => {
if (res.status === 200) {
let detailedRating = res.data.data;
this.setState({
detailedRating
})
}
}).catch(function (error) {
if (error.response.status === 401) {
window.location.href = window.location.origin+'/store/login';
}
});
};
render() {
const detailedRating = this.state.detailedRating;
if(detailedRating ==null){
return null;
}
const totalCount = detailedRating.noOfUsers;
const ratingVariety = detailedRating.ratingVariety;
const ratingArray = [];
for (let [key, value] of Object.entries(ratingVariety)) {
ratingArray.push(value);
}
const maximumRating = Math.max(...ratingArray);
const ratingBarPercentages = [0,0,0,0,0];
if(maximumRating>0){
for(let i = 0; i<5; i++){
ratingBarPercentages[i] = (ratingVariety[(i+1).toString()])/maximumRating*100;
}
}
return (
<Row className="d-rating">
<div className="numeric-data">
<div className="rate">{detailedRating.ratingValue.toFixed(1)}</div>
<StarRatings
rating={detailedRating.ratingValue}
starRatedColor="#777"
starDimension = "16px"
starSpacing = "2px"
numberOfStars={5}
name='rating'
/>
<br/>
<Text type="secondary" className="people-count"><Icon type="team" /> {totalCount} total</Text>
</div>
<div className="bar-containers">
<div className="bar-container">
<span className="number">5</span>
<span className="bar rate-5" style={{width: ratingBarPercentages[4]+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">4</span>
<span className="bar rate-4" style={{width: ratingBarPercentages[3]+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">3</span>
<span className="bar rate-3" style={{width: ratingBarPercentages[2]+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">2</span>
<span className="bar rate-2" style={{width: ratingBarPercentages[1]+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">1</span>
<span className="bar rate-1" style={{width: ratingBarPercentages[0]+"%"}}> </span>
</div>
</div>
</Row>
);
}
}
export default withConfigContext(DetailedRating);

@ -0,0 +1,171 @@
/*
* 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 {Divider, Row, Col, Typography, Button, Rate, notification} from "antd";
import "../../../App.css";
import ImgViewer from "../../apps/release/images/ImgViewer";
import StarRatings from "react-star-ratings";
import DetailedRating from "./DetailedRating";
import Reviews from "./review/Reviews";
import axios from "axios";
import AppInstallModal from "./install/AppInstallModal";
import CurrentUsersReview from "./review/CurrentUsersReview";
import {withConfigContext} from "../../../context/ConfigContext";
const {Title, Text, Paragraph} = Typography;
class ReleaseView extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
appInstallModalVisible: false
}
}
installApp = (type, payload) => {
const config = this.props.context;
const release = this.props.app.applicationReleases[0];
const {uuid} = release;
this.setState({
loading: true,
});
const url = window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.store + "/subscription/" + uuid + "/" + type + "/install";
axios.post(
url,
payload,
{
headers: {'X-Platform': config.serverConfig.platform}
}
).then(res => {
if (res.status === 200) {
this.setState({
loading: false,
appInstallModalVisible: false
});
notification["success"]({
message: 'Done!',
description:
'App installed triggered.',
});
} else {
this.setState({
loading: false
});
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while installing app",
});
}
}).catch((error) => {
if (error.response.status === 401) {
window.location.href = window.location.origin+ '/store/login';
} else {
this.setState({
loading: false,
visible: false
});
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while installing the app.",
});
}
});
};
showAppInstallModal = () => {
this.setState({
appInstallModalVisible: true
});
};
closeAppInstallModal = () => {
this.setState({
appInstallModalVisible: false
});
};
render() {
const {app,deviceType} = this.props;
const release = app.applicationReleases[0];
return (
<div>
<AppInstallModal
uuid={release.uuid}
visible={this.state.appInstallModalVisible}
deviceType = {deviceType}
onClose={this.closeAppInstallModal}
onInstall={this.installApp}/>
<div className="release">
<Row>
<Col xl={4} sm={6} xs={8} className="release-icon">
<img src={release.iconPath} alt="icon"/>
</Col>
<Col xl={10} sm={11} className="release-title">
<Title level={2}>{app.name}</Title>
<Text>Version : {release.version}</Text><br/><br/>
<StarRatings
rating={app.rating}
starRatedColor="#777"
starDimension="20px"
starSpacing="2px"
numberOfStars={5}
name='rating'
/>
</Col>
<Col xl={8} md={10} sm={24} xs={24} style={{float: "right"}}>
<div>
<Button.Group style={{float: "right"}}>
<Button onClick={this.showAppInstallModal} loading={this.state.loading}
htmlType="button" type="primary" icon="download">Install</Button>
</Button.Group>
</div>
</Col>
</Row>
<Divider/>
<Row>
<ImgViewer images={release.screenshots}/>
</Row>
<Divider/>
<Paragraph type="secondary" ellipsis={{rows: 3, expandable: true}}>
{release.description}
</Paragraph>
<Divider/>
<CurrentUsersReview uuid={release.uuid}/>
<Divider dashed={true}/>
<Text>REVIEWS</Text>
<Row>
<Col lg={18} md={24}>
<DetailedRating type="app" uuid={release.uuid}/>
</Col>
</Row>
<Reviews type="app" uuid={release.uuid}/>
</div>
</div>
);
}
}
export default withConfigContext(ReleaseView);

@ -0,0 +1,63 @@
/*
* 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, {Component} from 'react';
import RcViewer from 'rc-viewer';
import {Col} from "antd";
class ImgViewer extends Component {
render() {
const options = {
title: false,
toolbar: {
zoomIn: 0,
zoomOut: 0,
oneToOne: 0,
reset: 0,
prev: 1,
play: {
show: 0
},
next: 1,
rotateLeft: 0,
rotateRight: 0,
flipHorizontal: 0,
flipVertical: 0
},
rotatable: false,
transition: false,
movable : false
};
return (
<div>
<RcViewer options={options} ref='viewer'>
{this.props.images.map((screenshotUrl) => {
return (
<Col key={"col-" + screenshotUrl} lg={6} md={8} xs={8} className="release-screenshot">
<img key={screenshotUrl} src={screenshotUrl}/>
</Col>
)
})}
</RcViewer>
</div>
);
}
}
export default ImgViewer;

@ -0,0 +1,62 @@
/*
* 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, Tabs} from "antd";
import UserInstall from "./UserInstall";
import GroupInstall from "./GroupInstall";
import RoleInstall from "./RoleInstall";
import DeviceInstall from "./DeviceInstall";
const { TabPane } = Tabs;
class AppInstallModal extends React.Component{
state={
data:[]
};
render() {
const {deviceType} = this.props;
return (
<div>
<Modal
title="Install App"
visible={this.props.visible}
onCancel={this.props.onClose}
footer={null}
>
<Tabs defaultActiveKey="device">
<TabPane tab="Device" key="device">
<DeviceInstall deviceType={deviceType} onInstall={this.props.onInstall}/>
</TabPane>
<TabPane tab="User" key="user">
<UserInstall onInstall={this.props.onInstall}/>
</TabPane>
<TabPane tab="Role" key="role">
<RoleInstall onInstall={this.props.onInstall}/>
</TabPane>
<TabPane tab="Group" key="group">
<GroupInstall onInstall={this.props.onInstall}/>
</TabPane>
</Tabs>
</Modal>
</div>
);
}
}
export default AppInstallModal;

@ -0,0 +1,248 @@
/*
* 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 {Button, message, notification, Table, Typography} from "antd";
import TimeAgo from 'javascript-time-ago'
// Load locale-specific relative date/time formatting rules.
import en from 'javascript-time-ago/locale/en'
import {withConfigContext} from "../../../../context/ConfigContext";
const {Text} = Typography;
const columns = [
{
title: 'Device',
dataIndex: 'name',
fixed: 'left',
width: 100,
},
{
title: 'Modal',
dataIndex: 'deviceInfo',
key: 'modal',
render: deviceInfo => `${deviceInfo.vendor} ${deviceInfo.deviceModel}`
// todo add filtering options
},
{
title: 'Owner',
dataIndex: 'enrolmentInfo',
key: 'owner',
render: enrolmentInfo => enrolmentInfo.owner
// todo add filtering options
},
{
title: 'Last Updated',
dataIndex: 'enrolmentInfo',
key: 'dateOfLastUpdate',
render: (data) => {
return (getTimeAgo(data.dateOfLastUpdate));
}
// todo add filtering options
},
{
title: 'Status',
dataIndex: 'enrolmentInfo',
key: 'status',
render: enrolmentInfo => enrolmentInfo.status
// todo add filtering options
},
{
title: 'Ownership',
dataIndex: 'enrolmentInfo',
key: 'ownership',
render: enrolmentInfo => enrolmentInfo.ownership
// todo add filtering options
},
{
title: 'OS Version',
dataIndex: 'deviceInfo',
key: 'osVersion',
render: deviceInfo => deviceInfo.osVersion
// todo add filtering options
},
{
title: 'IMEI',
dataIndex: 'properties',
key: 'imei',
render: properties => {
let imei = "not-found";
for (let i = 0; i < properties.length; i++) {
if (properties[i].name === "IMEI") {
imei = properties[i].value;
}
}
return imei;
}
// todo add filtering options
},
];
const getTimeAgo = (time) => {
const timeAgo = new TimeAgo('en-US');
return timeAgo.format(time);
};
class DeviceInstall extends React.Component {
constructor(props) {
super(props);
TimeAgo.addLocale(en);
this.state = {
data: [],
pagination: {},
loading: false,
selectedRows: []
};
}
rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
// console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
this.setState({
selectedRows: selectedRows
})
},
getCheckboxProps: record => ({
disabled: record.name === 'Disabled User', // Column configuration not to be checked
name: record.name,
}),
};
componentDidMount() {
this.fetch();
}
//fetch data from api
fetch = (params = {}) => {
const config = this.props.context;
this.setState({loading: true});
const {deviceType} = this.props;
// get current page
const currentPage = (params.hasOwnProperty("page")) ? params.page : 1;
const extraParams = {
offset: 10 * (currentPage - 1), //calculate the offset
limit: 10,
status: "ACTIVE",
requireDeviceInfo: true,
type: deviceType
};
// note: encode with '%26' not '&'
const encodedExtraParams = Object.keys(extraParams).map(key => key + '=' + extraParams[key]).join('&');
//send request to the invoker
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt +
"/devices?" + encodedExtraParams,
).then(res => {
if (res.status === 200) {
const pagination = {...this.state.pagination};
this.setState({
loading: false,
data: res.data.data.devices,
pagination,
});
}
}).catch((error) => {
console.log(error);
if (error.hasOwnProperty("status") && error.response.status === 401) {
//todo display a popop with error
message.error('You are not logged in');
window.location.href = window.location.origin + '/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load devices.",
});
}
this.setState({loading: false});
});
};
handleTableChange = (pagination, filters, sorter) => {
const pager = {...this.state.pagination};
pager.current = pagination.current;
this.setState({
pagination: pager,
});
this.fetch({
results: pagination.pageSize,
page: pagination.current,
sortField: sorter.field,
sortOrder: sorter.order,
...filters,
});
};
install = () => {
const {selectedRows} = this.state;
const payload = [];
selectedRows.map(device => {
payload.push({
id: device.deviceIdentifier,
type: device.type
});
});
this.props.onInstall("devices", payload);
};
render() {
const {data, pagination, loading, selectedRows} = this.state;
return (
<div>
<Text>
Start installing the application for one or more users by entering the corresponding user name.
Select install to automatically start downloading the application for the respective user/users.
</Text>
<Table
style={{paddingTop: 20}}
columns={columns}
rowKey={record => record.deviceIdentifier}
dataSource={data}
pagination={{
...pagination,
size: "small",
// position: "top",
showTotal: (total, range) => `showing ${range[0]}-${range[1]} of ${total} devices`
// showQuickJumper: true
}}
loading={loading}
onChange={this.handleTableChange}
rowSelection={this.rowSelection}
scroll={{x: 1000}}
/>
<div style={{paddingTop: 10, textAlign: "right"}}>
<Button disabled={selectedRows.length === 0} htmlType="button" type="primary"
onClick={this.install}>
Install
</Button>
</div>
</div>
);
}
}
export default withConfigContext(DeviceInstall);

@ -0,0 +1,133 @@
/*
* 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 {Typography, Select, Spin, message, notification, Button} from "antd";
import debounce from 'lodash.debounce';
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
const {Text} = Typography;
const {Option} = Select;
class GroupInstall extends React.Component {
constructor(props) {
super(props);
this.lastFetchId = 0;
this.fetchUser = debounce(this.fetchUser, 800);
}
state = {
data: [],
value: [],
fetching: false,
};
fetchUser = value => {
this.lastFetchId += 1;
const fetchId = this.lastFetchId;
const config = this.props.context;
this.setState({data: [], fetching: true});
axios.post(
window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt+"/groups?name=" + value,
).then(res => {
if (res.status === 200) {
if (fetchId !== this.lastFetchId) {
// for fetch callback order
return;
}
const data = res.data.data.deviceGroups.map(group => ({
text: group.name,
value: group.name,
}));
this.setState({data, fetching: false});
}
}).catch((error) => { console.log(error);
if (error.hasOwnProperty("status") && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin+'/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load groups.",
});
}
this.setState({fetching: false});
});
};
handleChange = value => {
this.setState({
value,
data: [],
fetching: false,
});
};
install = () =>{
const {value} = this.state;
const data = [];
value.map(val=>{
data.push(val.key);
});
this.props.onInstall("group",data);
};
render() {
const {fetching, data, value} = this.state;
return (
<div>
<Text>Start installing the application for one or more groups by entering the corresponding group name. Select install to automatically start downloading the application for the respective device group/ groups.</Text>
<br/>
<br/>
<Select
mode="multiple"
labelInValue
value={value}
placeholder="Search groups"
notFoundContent={fetching ? <Spin size="small"/> : null}
filterOption={false}
onSearch={this.fetchUser}
onChange={this.handleChange}
style={{width: '100%'}}
>
{data.map(d => (
<Option key={d.value}>{d.text}</Option>
))}
</Select>
<div style={{paddingTop:10, textAlign:"right"}}>
<Button disabled={value.length===0} htmlType="button" type="primary" onClick={this.install}>Install</Button>
</div>
</div>
);
}
}
export default withConfigContext(GroupInstall);

@ -0,0 +1,133 @@
/*
* 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 {Typography, Select, Spin, message, notification, Button} from "antd";
import debounce from 'lodash.debounce';
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
const {Text} = Typography;
const {Option} = Select;
class RoleInstall extends React.Component {
constructor(props) {
super(props);
this.lastFetchId = 0;
this.fetchUser = debounce(this.fetchUser, 800);
}
state = {
data: [],
value: [],
fetching: false,
};
fetchUser = value => {
const config = this.props.context;
this.lastFetchId += 1;
const fetchId = this.lastFetchId;
this.setState({data: [], fetching: true});
axios.get(
window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt+"/roles?filter=" + value,
).then(res => {
if (res.status === 200) {
if (fetchId !== this.lastFetchId) {
// for fetch callback order
return;
}
const data = res.data.data.roles.map(role => ({
text: role,
value: role,
}));
this.setState({data, fetching: false});
}
}).catch((error) => { console.log(error);
if (error.hasOwnProperty("status") && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin+'/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load roles.",
});
}
this.setState({fetching: false});
});
};
handleChange = value => {
this.setState({
value,
data: [],
fetching: false,
});
};
install = () =>{
const {value} = this.state;
const data = [];
value.map(val=>{
data.push(val.key);
});
this.props.onInstall("role",data);
};
render() {
const {fetching, data, value} = this.state;
return (
<div>
<Text>Start installing the application for one or more roles by entering the corresponding role name. Select install to automatically start downloading the application for the respective user role/roles.</Text>
<br/>
<br/>
<Select
mode="multiple"
labelInValue
value={value}
placeholder="Search roles"
notFoundContent={fetching ? <Spin size="small"/> : null}
filterOption={false}
onSearch={this.fetchUser}
onChange={this.handleChange}
style={{width: '100%'}}
>
{data.map(d => (
<Option key={d.value}>{d.text}</Option>
))}
</Select>
<div style={{paddingTop:10, textAlign:"right"}}>
<Button disabled={value.length===0} htmlType="button" type="primary" onClick={this.install}>Install</Button>
</div>
</div>
);
}
}
export default withConfigContext(RoleInstall);

@ -0,0 +1,133 @@
/*
* 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 {Typography, Select, Spin, message, notification, Button} from "antd";
import debounce from 'lodash.debounce';
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
const {Text} = Typography;
const {Option} = Select;
class UserInstall extends React.Component {
constructor(props) {
super(props);
this.lastFetchId = 0;
this.fetchUser = debounce(this.fetchUser, 800);
}
state = {
data: [],
value: [],
fetching: false,
};
fetchUser = value => {
const config = this.props.context;
this.lastFetchId += 1;
const fetchId = this.lastFetchId;
this.setState({data: [], fetching: true});
//send request to the invoker
axios.get(
window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt+"/users/search?username=" + value,
).then(res => {
if (res.status === 200) {
if (fetchId !== this.lastFetchId) {
// for fetch callback order
return;
}
const data = res.data.data.users.map(user => ({
text: user.username,
value: user.username,
}));
this.setState({data, fetching: false});
}
}).catch((error) => {
if (error.response.hasOwnProperty(status) && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin+ '/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load users.",
});
}
this.setState({fetching: false});
});
};
handleChange = value => {
this.setState({
value,
data: [],
fetching: false,
});
};
install = () => {
const {value} = this.state;
const data = [];
value.map(val => {
data.push(val.key);
});
this.props.onInstall("user", data);
};
render() {
const {fetching, data, value} = this.state;
return (
<div>
<Text>Start installing the application for one or more users by entering the corresponding user name. Select install to automatically start downloading the application for the respective user/users. </Text>
<p>Select users</p>
<Select
mode="multiple"
labelInValue
value={value}
placeholder="Enter the username"
notFoundContent={fetching ? <Spin size="small"/> : null}
filterOption={false}
onSearch={this.fetchUser}
onChange={this.handleChange}
style={{width: '100%'}}
>
{data.map(d => (
<Option key={d.value}>{d.text}</Option>
))}
</Select>
<div style={{paddingTop: 10, textAlign: "right"}}>
<Button disabled={value.length===0} htmlType="button" type="primary" onClick={this.install}>Install</Button>
</div>
</div>
);
}
}
export default withConfigContext(UserInstall);

@ -0,0 +1,181 @@
/*
* 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 {Drawer, Button, Icon, Row, Col, Typography, Divider, Input, Spin, notification} from 'antd';
import StarRatings from "react-star-ratings";
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
const {Title} = Typography;
const {TextArea} = Input;
class AddReview extends React.Component {
state = {
visible: false,
content: '',
rating: 0,
loading: false
};
showDrawer = () => {
this.setState({
visible: true,
content: '',
rating: 0,
loading: false
});
};
onClose = () => {
this.setState({
visible: false,
});
};
changeRating = (newRating, name) => {
this.setState({
rating: newRating
});
};
onChange = (e) => {
this.setState({content: e.target.value})
};
onSubmit = () => {
const config = this.props.context;
const {content, rating} = this.state;
const {uuid} = this.props;
this.setState({
loading: true
});
const payload = {
content: content,
rating: rating
};
axios.post(
window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.store + "/reviews/" + uuid,
payload,
).then(res => {
if (res.status === 201) {
this.setState({
loading: false,
visible: false
});
notification["success"]({
message: 'Done!',
description:
'Your review has been posted successfully.',
});
setTimeout(() => {
window.location.href = uuid;
}, 2000)
} else {
this.setState({
loading: false,
visible: false
});
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"We are unable to add your review right now.",
});
}
}).catch((error) => {
if (error.response.status === 401) {
window.location.href = window.location.origin+ '/store/login';
} else {
this.setState({
loading: false,
visible: false
});
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"We are unable to add your review right now.",
});
}
});
};
render() {
return (
<div>
<Button type="primary" onClick={this.showDrawer}>
<Icon type="star"/> Add a review
</Button>
<Drawer
// title="Basic Drawer"
placement="bottom"
closable={false}
onClose={this.onClose}
visible={this.state.visible}
height={400}
>
<Spin spinning={this.state.loading} tip="Posting your review...">
<Row>
<Col lg={8}/>
<Col lg={8}>
<Title level={4}>Add review</Title>
<Divider/>
<TextArea
placeholder="Tell others what you think about this app. Would you recommend it, and why?"
onChange={this.onChange}
rows={4}
value={this.state.content || ''}
style={{marginBottom: 20}}
/>
<StarRatings
rating={this.state.rating}
changeRating={this.changeRating}
starRatedColor="#777"
starHoverColor="#444"
starDimension="20px"
starSpacing="2px"
numberOfStars={5}
name='rating'
/>
<br/><br/>
<Button onClick={this.onClose} style={{marginRight: 8}}>
Cancel
</Button>
<Button disabled={this.state.rating === 0} onClick={this.onSubmit} type="primary">
Submit
</Button>
</Col>
</Row>
</Spin>
</Drawer>
</div>
);
}
}
export default withConfigContext(AddReview);

@ -0,0 +1,128 @@
/*
* 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 {List, message, Typography, Empty, Button, Row, Col, notification} from "antd";
import SingleReview from "./singleReview/SingleReview";
import axios from "axios";
import AddReview from "./AddReview";
import {withConfigContext} from "../../../../context/ConfigContext";
const {Text, Paragraph} = Typography;
class CurrentUsersReview extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
componentDidMount() {
this.fetchData();
}
fetchData = () => {
const {uuid} = this.props;
const config = this.props.context;
axios.get(
window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.store + "/reviews/app/user/" + uuid,
).then(res => {
if (res.status === 200) {
const data = res.data.data.data;
this.setState({data});
}
}).catch((error) => {
if (error.response.hasOwnProperty(status) && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin+ '/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to get your review.",
});
}
});
};
deleteCallback = () =>{
this.setState({
data: []
});
};
addCallBack =(review) =>{
};
render() {
const {data} = this.state;
const {uuid} = this.props;
return (
<div>
<Text>MY REVIEW</Text>
<div style={{
overflow: "auto",
paddingTop: 8,
paddingLeft: 24
}}>
{data.length > 0 && (
<div>
<List
dataSource={data}
renderItem={item => (
<List.Item key={item.id}>
<SingleReview uuid={uuid} review={item} isDeletable={true} isEditable={true} deleteCallback={this.deleteCallback} isPersonalReview={true}/>
</List.Item>
)}
>
</List>
</div>
)}
{data.length === 0 && (
<div>
<Empty
image={Empty.PRESENTED_IMAGE_DEFAULT}
imagestyle={{
height: 60,
}}
description={
<span>Share your experience with your community by adding a review.</span>
}
>
{/*<Button type="primary">Add review</Button>*/}
<AddReview uuid={uuid}/>
</Empty>
</div>
)}
</div>
</div>
);
}
}
export default withConfigContext(CurrentUsersReview);

@ -0,0 +1,34 @@
/*
* 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.
*/
.infinite-container {
overflow: auto;
padding: 8px 24px;
}
.loading-container {
position: absolute;
bottom: 40px;
width: 100%;
text-align: center;
}
.loading {
position: absolute;
bottom: -40px;
left: 50%;
}

@ -0,0 +1,155 @@
/*
* 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 {List, message, Avatar, Spin, Button, notification} from 'antd';
import "./Reviews.css";
import InfiniteScroll from 'react-infinite-scroller';
import SingleReview from "./singleReview/SingleReview";
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
const limit = 5;
class Reviews extends React.Component {
state = {
data: [],
loading: false,
hasMore: false,
loadMore: false
};
componentDidMount() {
this.fetchData(0, limit, res => {
this.setState({
data: res,
});
});
}
fetchData = (offset, limit, callback) => {
const {uuid, type} = this.props;
const config = this.props.context;
axios.get(
window.location.origin+ config.serverConfig.invoker.uri +config.serverConfig.invoker.store+"/reviews/"+type+"/"+uuid,
{
headers: {'X-Platform': config.serverConfig.platform}
}).then(res => {
if (res.status === 200) {
let reviews = res.data.data.data;
callback(reviews);
}
}).catch(function (error) {
if (error.response.status === 401) {
window.location.href = window.location.origin+ '/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load reviews.",
});
}
});
};
handleInfiniteOnLoad = (count) => {
const offset = count * limit;
let data = this.state.data;
this.setState({
loading: true,
});
if (data.length > 149) {
this.setState({
hasMore: false,
loading: false,
});
return;
}
this.fetchData(offset, limit, res => {
if (res.length > 0) {
data = data.concat(res);
this.setState({
data,
loading: false,
});
} else {
this.setState({
hasMore: false,
loading: false
});
}
});
};
enableLoading = () => {
this.setState({
hasMore: true,
loadMore: true
});
};
deleteCallback = () =>{
this.fetchData(0, limit, res => {
this.setState({
data: res,
});
});
};
render() {
const {loading, hasMore, data, loadMore} = this.state;
const {uuid} = this.props;
return (
<div className="infinite-container">
<InfiniteScroll
initialLoad={false}
pageStart={0}
loadMore={this.handleInfiniteOnLoad}
hasMore={!loading && hasMore}
useWindow={true}
>
<List
dataSource={data}
renderItem={item => (
<List.Item key={item.id}>
<SingleReview uuid={uuid} review={item} isDeletable={true} isEditable={false} deleteCallback={this.deleteCallback}/>
</List.Item>
)}
>
{loading && hasMore && (
<div className="loading-container">
<Spin/>
</div>
)}
</List>
</InfiniteScroll>
{!loadMore && (data.length >= limit) && (<div style={{textAlign: "center"}}>
<Button type="dashed" htmlType="button" onClick={this.enableLoading}>Read All Reviews</Button>
</div>)}
</div>
);
}
}
export default withConfigContext(Reviews);

@ -0,0 +1,44 @@
/*
* 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.
*/
img.twemoji {
height: 1em;
width: 1em;
margin: 0 .05em 0 .1em;
vertical-align: -0.1em;
}
.edit-button {
color: #1890ff;
text-decoration: none;
outline: none;
cursor: pointer;
font-weight: normal;
font-size: 0.8em;
padding-left: 5px;
}
.delete-button {
color: #e74c3c;
text-decoration: none;
outline: none;
cursor: pointer;
font-weight: normal;
font-size: 0.8em;
padding-left: 5px;
}

@ -0,0 +1,152 @@
/*
* 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 {Avatar, notification} from "antd";
import {List, Typography, Popconfirm} from "antd";
import StarRatings from "react-star-ratings";
import Twemoji from "react-twemoji";
import "./SingleReview.css";
import EditReview from "./editReview/EditReview";
import axios from "axios";
import {withConfigContext} from "../../../../../context/ConfigContext";
const {Text, Paragraph} = Typography;
const colorList = ['#f0932b', '#badc58', '#6ab04c', '#eb4d4b', '#0abde3', '#9b59b6', '#3498db', '#22a6b3', '#e84393', '#f9ca24'];
class SingleReview extends React.Component {
static defaultProps = {
isPersonalReview: false
};
constructor(props) {
super(props);
const {username} = this.props.review;
const color = colorList[username.length % 10];
this.state = {
content: '',
rating: 0,
color: color,
review: props.review
}
}
updateCallback = (review) => {
this.setState({
review
});
};
deleteReview = () => {
const {uuid} = this.props;
const {id} = this.state.review;
const config = this.props.context;
let url =window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.store;
// call as an admin api if the review is not a personal review
if (!this.props.isPersonalReview) {
url += "/admin";
}
url += "/reviews/" + uuid + "/" + id;
axios.delete(url).then(res => {
if (res.status === 200) {
notification["success"]({
message: 'Done!',
description:
'The review has been deleted successfully.',
});
this.props.deleteCallback(id);
}
}).catch((error) => {
console.log(error);
if (error.hasOwnProperty("response") && error.response.status === 401) {
window.location.href = window.location.origin+ '/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"We were unable to delete the review..",
});
}
});
};
render() {
const {isEditable, isDeletable, uuid} = this.props;
const {color, review} = this.state;
const {content, rating, username} = review;
const avatarLetter = username.charAt(0).toUpperCase();
const body = (
<div style={{marginTop: -5}}>
<StarRatings
rating={rating}
starRatedColor="#777"
starDimension="12px"
starSpacing="2px"
numberOfStars={5}
name='rating'
/>
<Text style={{fontSize: 12, color: "#aaa"}} type="secondary"> {review.createdAt}</Text><br/>
<Paragraph style={{color: "#777"}}>
<Twemoji options={{className: 'twemoji'}}>
{content}
</Twemoji>
</Paragraph>
</div>
);
const title = (
<div>
{review.username}
{isEditable && (<EditReview uuid={uuid} review={review} updateCallback={this.updateCallback}/>)}
{isDeletable && (
<Popconfirm
title="Are you sure delete this review?"
onConfirm={this.deleteReview}
okText="Yes"
cancelText="No"
>
<span className="delete-button">delete</span>
</Popconfirm>)}
</div>
);
return (
<div>
<List.Item.Meta
avatar={
<Avatar style={{backgroundColor: color, verticalAlign: 'middle'}} size="large">
{avatarLetter}
</Avatar>
}
title={title}
description={body}
/>
</div>
);
}
}
export default withConfigContext(SingleReview);

@ -0,0 +1,27 @@
/*
* 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.
*/
.edit-button {
color: #1890ff;
text-decoration: none;
outline: none;
cursor: pointer;
font-weight: normal;
font-size: 0.8em;
padding-left: 5px;
}

@ -0,0 +1,191 @@
/*
* 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 {Drawer, Button, Icon, Row, Col, Typography, Divider, Input, Spin, notification} from 'antd';
import StarRatings from "react-star-ratings";
import axios from "axios";
import "./EditReview.css";
import {withConfigContext} from "../../../../../../context/ConfigContext";
const {Title} = Typography;
const {TextArea} = Input;
class EditReview extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
content: '',
rating: 0,
loading: false
};
}
componentDidMount() {
const {content,rating,id} = this.props.review;
console.log(this.props.review);
this.setState({
content,
rating
});
}
showDrawer = () => {
this.setState({
visible: true,
loading: false
});
};
onClose = () => {
this.setState({
visible: false,
});
};
changeRating = (newRating, name) => {
this.setState({
rating: newRating
});
};
onChange = (e) => {
this.setState({content: e.target.value})
};
onSubmit = () => {
const config = this.props.context;
const {content, rating} = this.state;
const {id} = this.props.review;
const {uuid} = this.props;
this.setState({
loading: true
});
const payload = {
content: content,
rating: rating
};
axios.put(
window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.store + "/reviews/" + uuid+"/"+id,
payload,
).then(res => {
if (res.status === 200) {
this.setState({
loading: false,
visible: false
});
notification["success"]({
message: 'Done!',
description:
'Your review has been update successfully.',
});
this.props.updateCallback(res.data.data);
} else {
this.setState({
loading: false,
visible: false
});
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"We are unable to update your review right now.",
});
}
}).catch((error) => {
console.log(error);
if (error.hasOwnProperty("response") && error.response.status === 401) {
window.location.href = window.location.origin+ '/store/login';
} else {
this.setState({
loading: false,
visible: false
});
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"We are unable to add your review right now.",
});
}
});
};
render() {
return (
<span>
<span className="edit-button" onClick={this.showDrawer}>edit</span>
<Drawer
// title="Basic Drawer"
placement="bottom"
closable={false}
onClose={this.onClose}
visible={this.state.visible}
height={400}
>
<Spin spinning={this.state.loading} tip="Posting your review...">
<Row>
<Col lg={8}/>
<Col lg={8}>
<Title level={4}>Edit review</Title>
<Divider/>
<TextArea
placeholder="Tell others what you think about this app. Would you recommend it, and why?"
onChange={this.onChange}
rows={6}
value={this.state.content || ''}
style={{marginBottom: 20}}
/>
<StarRatings
rating={this.state.rating}
changeRating={this.changeRating}
starRatedColor="#777"
starHoverColor="#444"
starDimension="20px"
starSpacing="2px"
numberOfStars={5}
name='rating'
/>
<br/><br/>
<Button onClick={this.onClose} style={{marginRight: 8}}>
Cancel
</Button>
<Button disabled={this.state.rating === 0} onClick={this.onSubmit} type="primary">
Submit
</Button>
</Col>
</Row>
</Spin>
</Drawer>
</span>
);
}
}
export default withConfigContext(EditReview);

@ -0,0 +1,34 @@
/*
* 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";
const ConfigContext = React.createContext();
export const withConfigContext = Component => {
return props => (
<ConfigContext.Consumer>
{context => {
return <Component {...props} context={context}/>;
}}
</ConfigContext.Consumer>
);
};
export default ConfigContext;

@ -0,0 +1,45 @@
/*
* 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.
*/
.App {
padding: 20px;
}
.ant-layout-header{
padding: 0;
height: auto;
box-shadow: 0 2px 8px #f0f1f2;
}
.steps-content {
margin-top: 16px;
border: 1px dashed #e9e9e9;
border-radius: 6px;
background-color: #fafafa;
min-height: 200px;
text-align: center;
padding-top: 80px;
}
.steps-action {
margin-top: 24px;
}
.ant-input-affix-wrapper .ant-input{
min-height: 0;
}

@ -0,0 +1,26 @@
<!--
~ 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.
-->
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"/>
<title>Entgra App Store</title>
</head>
<div id="root"></div>
</html>

@ -0,0 +1,62 @@
/*
* 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 ReactDOM from 'react-dom';
import * as serviceWorker from './serviceWorker';
import App from "./App";
import Login from "./pages/Login";
import Dashboard from "./pages/dashboard/Dashboard";
import Apps from "./pages/dashboard/apps/Apps";
import Release from "./pages/dashboard/apps/release/Release";
import './index.css';
const routes = [
{
path: '/store/login',
exact: true,
component: Login
},
{
path: '/store',
exact: false,
component: Dashboard,
routes: [
{
path: '/store/:deviceType',
component: Apps,
exact: true
},
{
path: '/store/:deviceType/apps/:uuid',
exact: true,
component: Release
}
]
}
];
ReactDOM.render(
<App routes={routes}/>,
document.getElementById('root'));
// If you want your app e and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@ -0,0 +1,62 @@
/*
* 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.
*/
@-moz-keyframes spin {
0% {
-moz-transform: rotate(0deg) scale(1.0);
}
100% {
-moz-transform: rotate(360deg) scale(0.1);
}
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg) scale(1.0);
}
100% {
-webkit-transform: rotate(360deg) scale(0.1);
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg) scale(1.0);
}
100% {
-webkit-transform: rotate(360deg) scale(0.1);
transform: rotate(360deg) scale(0.1);
}
}
.background {
position: absolute;
height: 100%;
width: 100%;
z-index: 0;
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
animation: spin 200s infinite linear;
}
.content {
position: relative;
z-index: 1;
}

@ -0,0 +1,171 @@
/*
* 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 {Typography, Row, Col, Form, Icon, Input, Button, Checkbox} from 'antd';
import './Login.css';
import axios from 'axios';
import {withConfigContext} from "../context/ConfigContext";
const {Title} = Typography;
const {Text} = Typography;
class Login extends React.Component {
render() {
const config = this.props.context;
return (
<div>
<div className="background">
</div>
<div className="content">
<Row>
<Col xs={3} sm={3} md={10}>
</Col>
<Col xs={18} sm={18} md={4}>
<Row style={{marginBottom: 20}}>
<Col style={{textAlign: "center"}}>
<img style={
{
marginTop: 36,
height: 60
}
}
src={config.theme.logo}/>
</Col>
</Row>
<Title level={2}>Login</Title>
<WrappedNormalLoginForm/>
</Col>
</Row>
<Row>
<Col span={4} offset={10}>
</Col>
</Row>
</div>
</div>
);
}
}
class NormalLoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
inValid: false,
loading: false
};
}
handleSubmit = (e) => {
const thisForm = this;
const config = this.props.context;
console.log(config);
e.preventDefault();
this.props.form.validateFields((err, values) => {
thisForm.setState({
inValid: false
});
if (!err) {
thisForm.setState({
loading: true
});
const parameters = {
username: values.username,
password: values.password,
platform: "store"
};
const request = Object.keys(parameters).map(key => key + '=' + parameters[key]).join('&');
axios.post(window.location.origin+ config.serverConfig.loginUri, request
).then(res => {
if (res.status === 200) {
window.location = window.location.origin+ "/store";
}
}).catch(function (error) {
if (error.response.status === 400) {
thisForm.setState({
inValid: true,
loading: false
});
}
});
}
});
};
render() {
const {getFieldDecorator} = this.props.form;
let errorMsg = "";
if (this.state.inValid) {
errorMsg = <Text type="danger">Invalid Login Details</Text>;
}
let loading = "";
if (this.state.loading) {
loading = <Text type="secondary">Loading..</Text>;
}
return (
<Form onSubmit={this.handleSubmit} className="login-form">
<Form.Item>
{getFieldDecorator('username', {
rules: [{required: true, message: 'Please input your username!'}],
})(
<Input name="username" style={{height: 32}}
prefix={<Icon type="user" style={{color: 'rgba(0,0,0,.25)'}}/>}
placeholder="Username"/>
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('password', {
rules: [{required: true, message: 'Please input your Password!'}],
})(
<Input name="password" style={{height: 32}}
prefix={<Icon type="lock" style={{color: 'rgba(0,0,0,.25)'}}/>} type="password"
placeholder="Password"/>
)}
</Form.Item>
{loading}
{errorMsg}
<Form.Item>
{getFieldDecorator('remember', {
valuePropName: 'checked',
initialValue: true,
})(
<Checkbox>Remember me</Checkbox>
)}
<br/>
<a className="login-form-forgot" href="">Forgot password</a>
<Button block type="primary" htmlType="submit" className="login-form-button">
Log in
</Button>
</Form.Item>
</Form>
);
}
}
const WrappedNormalLoginForm = withConfigContext(Form.create({name: 'normal_login'})(NormalLoginForm));
export default withConfigContext(Login);

@ -0,0 +1,157 @@
/*
* 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 {Layout, Menu, Icon} from 'antd';
const {Header, Content, Footer} = Layout;
import {Link} from "react-router-dom";
import RouteWithSubRoutes from "../../components/RouteWithSubRoutes";
import {Switch} from 'react-router';
import axios from "axios";
import "../../App.css";
import {withConfigContext} from "../../context/ConfigContext";
import Logout from "./logout/Logout";
const {SubMenu} = Menu;
class Dashboard extends React.Component {
constructor(props) {
super(props);
this.state = {
routes: props.routes,
selectedKeys: [],
deviceTypes: []
};
this.logo = this.props.context.theme.logo;
}
componentDidMount() {
this.getDeviceTypes();
}
getDeviceTypes = () => {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt + "/device-types"
).then(res => {
if (res.status === 200) {
const deviceTypes = JSON.parse(res.data.data);
this.setState({
deviceTypes,
loading: false,
});
}
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
window.location.href = window.location.origin + '/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load device types.",
});
}
this.setState({
loading: false
});
});
};
changeSelectedMenuItem = (key) => {
this.setState({
selectedKeys: [key]
})
};
render() {
const config = this.props.context;
const {selectedKeys, deviceTypes} = this.state;
return (
<div>
<Layout className="layout">
<Header style={{paddingLeft: 0, paddingRight: 0}}>
<div className="logo-image">
<img alt="logo" src={this.logo}/>
</div>
<Menu
theme="light"
mode="horizontal"
defaultSelectedKeys={selectedKeys}
style={{lineHeight: '64px'}}
>
{
deviceTypes.map((deviceType) => {
const platform = deviceType.name;
const defaultPlatformIcons = config.defaultPlatformIcons;
let icon = defaultPlatformIcons.default.icon;
let theme = defaultPlatformIcons.default.theme;
if (defaultPlatformIcons.hasOwnProperty(platform)) {
icon = defaultPlatformIcons[platform].icon;
theme = defaultPlatformIcons[platform].theme;
}
return (
<Menu.Item key={platform}>
<Link to={"/store/" + platform}>
<Icon type={icon} theme={theme}/>
{platform}
</Link>
</Menu.Item>
);
})
}
<Menu.Item key="web-clip"><Link to="/store/web-clip"><Icon type="upload"/>Web
Clips</Link></Menu.Item>
<SubMenu className="profile"
title={
<span className="submenu-title-wrapper">
<Icon type="user"/>
Profile
</span>
}
>
<Logout/>
</SubMenu>
</Menu>
</Header>
</Layout>
<Layout>
<Content style={{padding: '0 0'}}>
<Switch>
{this.state.routes.map((route) => (
<RouteWithSubRoutes changeSelectedMenuItem={this.changeSelectedMenuItem}
key={route.path} {...route} />
))}
</Switch>
</Content>
<Footer style={{textAlign: 'center'}}>
©2019 entgra.io
</Footer>
</Layout>
</div>
);
}
}
export default withConfigContext(Dashboard);

@ -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.
*/
.logo {
width: 120px;
height: 31px;
margin: 16px 0 16px 20px;
float: left;
img{
height: 35px;
}
}
input{
min-height: 0;
}

@ -0,0 +1,353 @@
/*
* 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,
Card,
Steps,
Button,
message,
Row,
Col,
Tag,
Tooltip,
Input,
Icon,
Select,
Switch,
Form,
Upload,
Divider
} from "antd";
import Step1 from "./Step1";
import Step2 from "./Step2";
import Step3 from "./Step3";
import styles from "./Style.less";
import IconImage from "./IconImg";
import UploadScreenshots from "./UploadScreenshots";
const Paragraph = Typography;
const Dragger = Upload.Dragger;
const routes = [
{
path: 'index',
breadcrumbName: 'store',
},
{
path: 'first',
breadcrumbName: 'dashboard',
},
{
path: 'second',
breadcrumbName: 'add new app',
},
];
const props = {
name: 'file',
multiple: false,
action: '//jsonplaceholder.typicode.com/posts/',
onChange(info) {
const status = info.file.status;
if (status !== 'uploading') {
// console.log(info.file, info.fileList);
}
if (status === 'done') {
message.success(`${info.file.name} file uploaded successfully.`);
} else if (status === 'error') {
message.error(`${info.file.name} file upload failed.`);
}
},
};
const Step = Steps.Step;
const steps = [{
title: 'First',
content: Step1
}, {
title: 'Second',
content: Step2,
}, {
title: 'Last',
content: Step3,
}];
const {Option} = Select;
const {TextArea} = Input;
const InputGroup = Input.Group;
const formItemLayout = {
labelCol: {
span: 4,
},
wrapperCol: {
span: 20,
},
};
class EditableTagGroup extends React.Component {
state = {
tags: [],
inputVisible: false,
inputValue: '',
};
handleClose = (removedTag) => {
const tags = this.state.tags.filter(tag => tag !== removedTag);
// console.log(tags);
this.setState({tags});
}
showInput = () => {
this.setState({inputVisible: true}, () => this.input.focus());
}
handleInputChange = (e) => {
this.setState({inputValue: e.target.value});
}
handleInputConfirm = () => {
const {inputValue} = this.state;
let {tags} = this.state;
if (inputValue && tags.indexOf(inputValue) === -1) {
tags = [...tags, inputValue];
}
// console.log(tags);
this.setState({
tags,
inputVisible: false,
inputValue: '',
});
}
saveInputRef = input => this.input = input
render() {
const {tags, inputVisible, inputValue} = this.state;
return (
<div>
{tags.map((tag, index) => {
const isLongTag = tag.length > 20;
const tagElem = (
<Tag key={tag} closable={index !== 0} onClose={() => this.handleClose(tag)}>
{isLongTag ? `${tag.slice(0, 20)}...` : tag}
</Tag>
);
return isLongTag ? <Tooltip title={tag} key={tag}>{tagElem}</Tooltip> : tagElem;
})}
{inputVisible && (
<Input
ref={this.saveInputRef}
type="text"
size="small"
style={{width: 78}}
value={inputValue}
onChange={this.handleInputChange}
onBlur={this.handleInputConfirm}
onPressEnter={this.handleInputConfirm}
/>
)}
{!inputVisible && (
<Tag
onClick={this.showInput}
style={{background: '#fff', borderStyle: 'dashed'}}
>
<Icon type="plus"/> New Tag
</Tag>
)}
</div>
);
}
}
class AddNewApp extends React.Component {
constructor(props) {
super(props);
this.state = {
current: 0,
};
}
tags = [];
addTag(key, value){
this.tags.push(<Option key={key}>{value}</Option>);
}
next() {
const current = this.state.current + 1;
this.setState({current});
}
prev() {
const current = this.state.current - 1;
this.setState({current});
}
render() {
const {current} = this.state;
const Content = steps[current].content;
this.addTag('1','Lorem');
this.addTag('2','Ipsum');
return (
<div>
<PageHeader
title="Add New App"
breadcrumb={{routes}}
>
<div className="wrap">
<div className="content">
<Paragraph>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempo.
</Paragraph>
</div>
</div>
</PageHeader>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 720}}>
<Row>
<Col span={20} offset={2}>
<Card>
<Row>
<Col span={12}>
<div>
<Form labelAlign="left" layout="horizontal" className={styles.stepForm}
hideRequiredMark>
<Form.Item {...formItemLayout} label="Platform">
<Select placeholder="ex: android">
<Option value="Android">Android</Option>
<Option value="iOS">iOS</Option>
</Select>
</Form.Item>
<Form.Item {...formItemLayout} label="Type">
<Select value="Enterprise">
<Option value="Enterprise" selected>Enterprise</Option>
</Select>
</Form.Item>
<Form.Item {...formItemLayout} label="App Name">
<Input placeholder="ex: Lorem App"/>
</Form.Item>
<Form.Item {...formItemLayout} label="Description">
<TextArea placeholder="Enter the description..." rows={7}/>
</Form.Item>
<Form.Item {...formItemLayout} label="Category">
<Select placeholder="Select a category">
<Option value="travel">Travel</Option>
<Option value="entertainment">Entertainment</Option>
</Select>
</Form.Item>
<Form.Item {...formItemLayout} label="Price">
<Input prefix="$" placeholder="00.00"/>
</Form.Item>
<Form.Item {...formItemLayout} label="Is Sahred?">
<Switch checkedChildren={<Icon type="check" />} unCheckedChildren={<Icon type="close" />} defaultChecked />
</Form.Item>
<Divider/>
<Form.Item {...formItemLayout} label="Tags">
<InputGroup>
<Row gutter={8}>
<Col span={22}>
<Select
mode="multiple"
style={{ width: '100%' }}
placeholder="Tags Mode"
>
{this.tags}
</Select>
</Col>
<Col span={2}>
<Button type="dashed" shape="circle" icon="plus"/>
</Col>
</Row>
</InputGroup>
</Form.Item>
<Form.Item {...formItemLayout} label="Meta Daa">
<InputGroup>
<Row gutter={8}>
<Col span={10}>
<Input placeholder="Key"/>
</Col>
<Col span={12}>
<Input placeholder="value"/>
</Col>
<Col span={2}>
<Button type="dashed" shape="circle" icon="plus"/>
</Col>
</Row>
</InputGroup>
</Form.Item>
</Form>
</div>
</Col>
<Col span={12} style={{paddingTop: 40, paddingLeft: 20}}>
<p>Application</p>
<div style={{height: 170}}>
<Dragger {...props}>
<p className="ant-upload-drag-icon">
<Icon type="inbox"/>
</p>
<p className="ant-upload-text">Click or drag file to this area to
upload</p>
<p className="ant-upload-hint">Support for a single or bulk upload.
Strictly prohibit from uploading company data or other band
files</p>
</Dragger>
</div>
<Row style={{marginTop: 40}}>
<Col span={12}>
<p>Icon</p>
<IconImage/>
</Col>
<Col span={12}>
<p>Banner</p>
<IconImage/>
</Col>
</Row>
<Row style={{marginTop: 40}}>
<Col span={24}>
<p>Screenshots</p>
<UploadScreenshots/>
</Col>
</Row>
</Col>
</Row>
</Card>
</Col>
</Row>
</div>
</div>
);
}
}
export default AddNewApp;

@ -0,0 +1,84 @@
/*
* 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 { Upload, Icon, message } from 'antd';
function getBase64(img, callback) {
const reader = new FileReader();
reader.addEventListener('load', () => callback(reader.result));
reader.readAsDataURL(img);
}
function beforeUpload(file) {
const isJPG = file.type === 'image/jpeg';
if (!isJPG) {
message.error('You can only upload JPG file!');
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('Image must smaller than 2MB!');
}
return isJPG && isLt2M;
}
class IconImage extends React.Component {
state = {
loading: false,
};
handleChange = (info) => {
if (info.file.status === 'uploading') {
this.setState({ loading: true });
return;
}
if (info.file.status === 'done') {
// Get this url from response in real world.
getBase64(info.file.originFileObj, imageUrl => this.setState({
imageUrl,
loading: false,
}));
}
}
render() {
const uploadButton = (
<div>
<Icon type={this.state.loading ? 'loading' : 'plus'} />
<div className="ant-upload-text">Upload</div>
</div>
);
const imageUrl = this.state.imageUrl;
return (
<Upload
name="avatar"
listType="picture-card"
className="avatar-uploader"
showUploadList={false}
action="//jsonplaceholder.typicode.com/posts/"
beforeUpload={beforeUpload}
onChange={this.handleChange}
>
{imageUrl ? <img src={imageUrl} alt="avatar" /> : uploadButton}
</Upload>
);
}
}
export default IconImage;

@ -0,0 +1,170 @@
/*
* 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 {Form, Input, Button, Select, Divider, Tag, Tooltip, Icon, Checkbox, Row, Col} from "antd";
import styles from './Style.less';
const { Option } = Select;
const { TextArea } = Input;
const InputGroup = Input.Group;
const formItemLayout = {
labelCol: {
span: 8,
},
wrapperCol: {
span: 16,
},
};
class EditableTagGroup extends React.Component {
state = {
tags: [],
inputVisible: false,
inputValue: '',
};
handleClose = (removedTag) => {
const tags = this.state.tags.filter(tag => tag !== removedTag);
// console.log(tags);
this.setState({ tags });
}
showInput = () => {
this.setState({ inputVisible: true }, () => this.input.focus());
}
handleInputChange = (e) => {
this.setState({ inputValue: e.target.value });
}
handleInputConfirm = () => {
const { inputValue } = this.state;
let { tags } = this.state;
if (inputValue && tags.indexOf(inputValue) === -1) {
tags = [...tags, inputValue];
}
// console.log(tags);
this.setState({
tags,
inputVisible: false,
inputValue: '',
});
}
saveInputRef = input => this.input = input
render() {
const { tags, inputVisible, inputValue } = this.state;
return (
<div>
{tags.map((tag, index) => {
const isLongTag = tag.length > 20;
const tagElem = (
<Tag key={tag} closable={index !== 0} onClose={() => this.handleClose(tag)}>
{isLongTag ? `${tag.slice(0, 20)}...` : tag}
</Tag>
);
return isLongTag ? <Tooltip title={tag} key={tag}>{tagElem}</Tooltip> : tagElem;
})}
{inputVisible && (
<Input
ref={this.saveInputRef}
type="text"
size="small"
style={{ width: 78 }}
value={inputValue}
onChange={this.handleInputChange}
onBlur={this.handleInputConfirm}
onPressEnter={this.handleInputConfirm}
/>
)}
{!inputVisible && (
<Tag
onClick={this.showInput}
style={{ background: '#fff', borderStyle: 'dashed' }}
>
<Icon type="plus" /> New Tag
</Tag>
)}
</div>
);
}
}
class Step1 extends React.Component {
render() {
return (
<div>
<Form layout="horizontal" className={styles.stepForm} hideRequiredMark>
<Form.Item {...formItemLayout} label="Platform">
<Select placeholder="ex: android">
<Option value="Android">Android</Option>
<Option value="iOS">iOS</Option>
</Select>
</Form.Item>
<Form.Item {...formItemLayout} label="Type">
<Select value="Enterprise">
<Option value="Enterprise" selected>Enterprise</Option>
</Select>
</Form.Item>
<Form.Item {...formItemLayout} label="Name">
<Input placeholder="App Name" />
</Form.Item>
<Form.Item {...formItemLayout} label="Description">
<TextArea placeholder="Enter the description" rows={4} />
</Form.Item>
<Form.Item {...formItemLayout} label="Category">
<Select placeholder="Select a category">
<Option value="travel">Travel</Option>
<Option value="entertainment">Entertainment</Option>
</Select>
</Form.Item>
<Form.Item {...formItemLayout} label="Tags">
<EditableTagGroup/>
</Form.Item>
<Form.Item {...formItemLayout} label="Price">
<Input prefix="$" placeholder="00.00" />
</Form.Item>
<Form.Item {...formItemLayout} label="Share with all tenents?">
<Checkbox > </Checkbox>
</Form.Item>
<Form.Item {...formItemLayout} label="Meta Daa">
<InputGroup>
<Row gutter={8}>
<Col span={5}>
<Input placeholder="Key" />
</Col>
<Col span={10}>
<Input placeholder="value" />
</Col>
<Col span={4}>
<Button type="dashed" shape="circle" icon="plus" />
</Col>
</Row>
</InputGroup>
</Form.Item>
</Form>
</div>
);
}
}
export default Step1;

@ -0,0 +1,29 @@
/*
* 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"
class Step2 extends React.Component {
render() {
return (
<p>tttoooeeee</p>
);
}
}
export default Step2;

@ -0,0 +1,29 @@
/*
* 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"
class Step3 extends React.Component {
render() {
return (
<p>tttoooeeee</p>
);
}
}
export default Step3;

@ -0,0 +1,22 @@
/*
* 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.
*/
.stepForm {
max-width: 500px;
margin: 40px auto 0;
}

@ -0,0 +1,67 @@
/*
* 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 { Upload, Icon, Modal} from 'antd';
class UploadScreenshots extends React.Component {
state = {
previewVisible: false,
previewImage: '',
fileList: [],
};
handleCancel = () => this.setState({ previewVisible: false });
handlePreview = (file) => {
this.setState({
previewImage: file.url || file.thumbUrl,
previewVisible: true,
});
};
handleChange = ({ fileList }) => this.setState({ fileList });
render() {
const { previewVisible, previewImage, fileList } = this.state;
const uploadButton = (
<div>
<Icon type="plus" />
<div className="ant-upload-text">Upload</div>
</div>
);
return (
<div className="clearfix">
<Upload
action="//jsonplaceholder.typicode.com/posts/"
listType="picture-card"
fileList={fileList}
onPreview={this.handlePreview}
onChange={this.handleChange}
>
{fileList.length >= 3 ? null : uploadButton}
</Upload>
<Modal visible={previewVisible} footer={null} onCancel={this.handleCancel}>
<img alt="example" style={{ width: '100%' }} src={previewImage} />
</Modal>
</div>
);
}
}
export default UploadScreenshots;

@ -0,0 +1,67 @@
/*
* 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 { Upload, Icon, Modal} from 'antd';
class AddTagModal extends React.Component {
state = {
previewVisible: false,
previewImage: '',
fileList: [],
};
handleCancel = () => this.setState({ previewVisible: false });
handlePreview = (file) => {
this.setState({
previewImage: file.url || file.thumbUrl,
previewVisible: true,
});
};
handleChange = ({ fileList }) => this.setState({ fileList });
render() {
const { previewVisible, previewImage, fileList } = this.state;
const uploadButton = (
<div>
<Icon type="plus" />
<div className="ant-upload-text">Upload</div>
</div>
);
return (
<div className="clearfix">
<Upload
action="//jsonplaceholder.typicode.com/posts/"
listType="picture-card"
fileList={fileList}
onPreview={this.handlePreview}
onChange={this.handleChange}
>
{fileList.length >= 3 ? null : uploadButton}
</Upload>
<Modal visible={previewVisible} footer={null} onCancel={this.handleCancel}>
<img alt="example" style={{ width: '100%' }} src={previewImage} />
</Modal>
</div>
);
}
}
export default AddTagModal;

@ -0,0 +1,45 @@
/*
* 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 AppList from "../../../components/apps/AppList";
class Apps extends React.Component {
routes;
constructor(props) {
super(props);
this.routes = props.routes;
}
render() {
const {deviceType} = this.props.match.params;
return (
<div>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 760}}>
{deviceType!==null && <AppList changeSelectedMenuItem={this.props.changeSelectedMenuItem} deviceType={deviceType}/>}
</div>
</div>
);
}
}
export default Apps;

@ -0,0 +1,131 @@
/*
* 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 '../../../../App.css';
import {Skeleton, Typography, Row, Col, Card, message, notification, Breadcrumb, Icon} from "antd";
import ReleaseView from "../../../../components/apps/release/ReleaseView";
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
import {Link} from "react-router-dom";
const {Title} = Typography;
class Release extends React.Component {
routes;
constructor(props) {
super(props);
this.routes = props.routes;
this.state = {
loading: true,
app: null,
uuid: null
};
}
componentDidMount() {
const {uuid, deviceType} = this.props.match.params;
this.fetchData(uuid);
this.props.changeSelectedMenuItem(deviceType);
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (prevState.uuid !== this.state.uuid) {
const {uuid, deviceType} = this.props.match.params;
this.fetchData(uuid);
this.props.changeSelectedMenuItem(deviceType);
}
}
fetchData = (uuid) => {
const config = this.props.context;
//send request to the invoker
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.store + "/applications/" + uuid,
).then(res => {
if (res.status === 200) {
let app = res.data.data;
this.setState({
app: app,
loading: false,
uuid: uuid
})
}
}).catch((error) => {
console.log(error);
if (error.hasOwnProperty("response") && error.response.status === 401) {
//todo display a popop with error
message.error('You are not logged in');
window.location.href = window.location.origin + '/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load releases.",
});
}
this.setState({loading: false});
});
};
render() {
const {app, loading} = this.state;
const {deviceType} = this.props.match.params;
let content = <Title level={3}>No Releases Found</Title>;
let appName = "loading...";
if (app != null && app.applicationReleases.length !== 0) {
content = <ReleaseView app={app} deviceType={deviceType}/>;
appName = app.name;
}
return (
<div style={{background: '#f0f2f5', minHeight: 780}}>
<Row style={{padding: 10}}>
<Col lg={4}>
</Col>
<Col lg={16} md={24} style={{padding: 3}}>
<Breadcrumb style={{paddingBottom: 16}}>
<Breadcrumb.Item>
<Link to={"/store/"+deviceType}><Icon type="home"/> {deviceType + " apps"} </Link>
</Breadcrumb.Item>
<Breadcrumb.Item>{appName}</Breadcrumb.Item>
</Breadcrumb>
<Card>
<Skeleton loading={loading} avatar={{size: 'large'}} active paragraph={{rows: 8}}>
{content}
</Skeleton>
</Card>
</Col>
</Row>
</div>
);
}
}
export default withConfigContext(Release);

@ -0,0 +1,80 @@
/*
* 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 {notification, Menu, Icon} from 'antd';
import axios from 'axios';
import {withConfigContext} from "../../../context/ConfigContext";
/*
This class for call the logout api by sending request
*/
class Logout extends React.Component {
constructor(props) {
super(props);
this.state = {
inValid: false,
loading: false
};
}
/*
This function call the logout api when the request is success
*/
handleSubmit = () => {
const thisForm = this;
const config = this.props.context;
thisForm.setState({
inValid: false
});
axios.post(window.location.origin + config.serverConfig.logoutUri
).then(res => {
//if the api call status is correct then user will logout and then it goes to login page
if (res.status === 200) {
window.location = window.location.origin + "/store/login";
}
}).catch(function (error) {
if (error.hasOwnProperty("response") && error.response.status === 400) {
thisForm.setState({
inValid: true
});
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to logout.",
});
}
});
};
render() {
return (
<Menu>
<Menu.Item key="1" onClick={this.handleSubmit}><Icon type="logout"/>Logout</Menu.Item>
</Menu>
);
}
}
export default withConfigContext(Logout);

@ -0,0 +1,153 @@
/*
* 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.
*/
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

@ -0,0 +1,134 @@
/*
* Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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.
*/
var path = require('path');
const HtmlWebPackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const configurations = require("./public/conf/config.json");
const config = {
devtool: "source-map",
output: {
publicPath: '/store/'
},
watch: false,
resolve: {
alias: {
AppData: path.resolve(__dirname, 'source/src/app/common/'),
AppComponents: path.resolve(__dirname, 'source/src/app/components/')
},
extensions: ['.jsx', '.js', '.ttf', '.woff', '.woff2', '.svg']
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader'
}
]
},
{
test: /\.html$/,
use: [
{
loader: "html-loader",
options: { minimize: true }
}
]
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"postcss-loader",
"sass-loader"
]
},
{
test: /\.scss$/,
use: [ 'style-loader', 'scss-loader' ]
},
{
test: /\.less$/,
use: [
{
loader: "style-loader"
},
{
loader: "css-loader",
},
{
loader: "less-loader",
options: {
modifyVars: {
'primary-color': configurations.theme.primaryColor,
'link-color': configurations.theme.primaryColor,
},
javascriptEnabled: true,
},
}
]
},
{
test: /\.(woff|woff2|eot|ttf|svg)$/,
loader: 'url-loader?limit=100000',
},
{
test: /\.(png|jpe?g)/i,
use: [
{
loader: "url-loader",
options: {
name: "./img/[name].[ext]",
limit: 10000
}
},
{
loader: "img-loader"
}
]
}
]
},
plugins: [
new HtmlWebPackPlugin({
template: "./src/index.html",
filename: "./index.html"
}),
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
})
],
externals: {
'Config': JSON.stringify(require('./public/conf/config.json'))
}
};
if (process.env.NODE_ENV === "development") {
config.watch = true;
}
module.exports = config;

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
~
~ WSO2 Inc. 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.
-->
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<display-name>Entgra-Webapp</display-name>
<error-page>
<error-code>404</error-code>
<location>/index.html</location>
</error-page>
</web-app>

@ -901,12 +901,7 @@ public interface DeviceManagementService {
required = true)
@PathParam("device-id")
@Size(max = 45)
String deviceId,
@ApiParam(
name = "permanentDelete",
value = "Boolean flag indicating whether to permanently delete the device.",
required = false)
@QueryParam("permanentDelete") boolean permanentDelete);
String deviceId);
@GET
@Path("/{type}/{id}/features")

@ -15,6 +15,22 @@
* specific language governing permissions and limitations
* under the License.
*
*
* Copyright (c) 2019, Entgra (pvt) Ltd. (https://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.mgt.jaxrs.service.api.admin;
@ -76,6 +92,12 @@ import java.util.List;
description = "Update the ownership of the device",
key = "perm:admin:devices:update-enrollment",
permissions = {"/device-mgt/admin/devices/update-enrollment"}
),
@Scope(
name = "Permanently Delete the device specified by device id",
description = "Permanently Delete the device specified by device id",
key = "perm:devices:permanent-delete",
permissions = {"/device-mgt/admin/devices/permanent-delete"}
)
}
)
@ -225,4 +247,72 @@ public interface DeviceManagementAdminService {
value = "List of device identifiers.",
required = true)
List<String> deviceIdentifiers);
@DELETE
@Path("/type/{device-type}/id/{device-id}")
@ApiOperation(
produces = MediaType.APPLICATION_JSON,
consumes = MediaType.APPLICATION_JSON,
httpMethod = "DELETE",
value = "Permanently remove the Device Specified by the Device ID",
notes = "Returns the status of the permanently deleted device operation and the details of the deleted device.",
tags = "Device Management",
extensions = {
@Extension(properties = {
@ExtensionProperty(name = Constants.SCOPE, value = "perm:devices:permanent-delete")
})
}
)
@ApiResponses(
value = {
@ApiResponse(
code = 200,
message = "OK. \n Successfully deleted the device permanently.",
response = Device.class,
responseHeaders = {
@ResponseHeader(
name = "Content-Type",
description = "The content type of the body"),
@ResponseHeader(
name = "ETag",
description = "Entity Tag of the response resource.\n" +
"Used by caches, or in conditional requests."),
@ResponseHeader(
name = "Last-Modified",
description = "Date and time the resource has been modified the last time.\n" +
"Used by caches, or in conditional requests."),
}),
@ApiResponse(
code = 304,
message = "Not Modified. Empty body because the client already has the latest " +
"version of the requested resource."),
@ApiResponse(
code = 400,
message = "Bad Request. \n Invalid request or validation error.",
response = ErrorResponse.class),
@ApiResponse(
code = 404,
message = "Not Found. \n No device is found under the provided type and id.",
response = ErrorResponse.class),
@ApiResponse(
code = 500,
message = "Internal Server Error. \n " +
"Server error occurred while retrieving information requested device.",
response = ErrorResponse.class)
})
Response deleteDevicePermanently(
@ApiParam(
name = "device-type",
value = "The device type, such as ios, android, or windows.",
required = true)
@PathParam("device-type")
@Size(max = 45)
String deviceType,
@ApiParam(
name = "device-id",
value = "The device identifier of the device.",
required = true)
@PathParam("device-id")
@Size(max = 45)
String deviceId);
}

@ -105,7 +105,6 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Path("/devices")
@Produces(MediaType.APPLICATION_JSON)
@ -326,8 +325,7 @@ public class DeviceManagementServiceImpl implements DeviceManagementService {
@Override
@Path("/type/{device-type}/id/{device-id}")
public Response deleteDevice(@PathParam("device-type") String deviceType,
@PathParam("device-id") String deviceId,
@QueryParam("permanentDelete") boolean permanentDelete) {
@PathParam("device-id") String deviceId) {
DeviceManagementProviderService deviceManagementProviderService =
DeviceMgtAPIUtils.getDeviceManagementService();
try {
@ -336,16 +334,8 @@ public class DeviceManagementServiceImpl implements DeviceManagementService {
if (persistedDevice == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
boolean response;
if (permanentDelete) {
response = deviceManagementProviderService.deleteDevice(deviceIdentifier);
} else {
response = deviceManagementProviderService.disenrollDevice(deviceIdentifier);
}
boolean response = deviceManagementProviderService.disenrollDevice(deviceIdentifier);
return Response.status(Response.Status.OK).entity(response).build();
} catch (DeviceManagementException e) {
String msg = "Error encountered while deleting device of type : " + deviceType + " and " +
"ID : " + deviceId;

@ -15,6 +15,22 @@
* specific language governing permissions and limitations
* under the License.
*
*
* Copyright (c) 2019, Entgra (pvt) Ltd. (https://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.mgt.jaxrs.service.impl.admin;
@ -24,10 +40,12 @@ import org.wso2.carbon.base.MultitenantConstants;
import org.wso2.carbon.context.CarbonContext;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.device.mgt.common.Device;
import org.wso2.carbon.device.mgt.common.DeviceIdentifier;
import org.wso2.carbon.device.mgt.common.exceptions.DeviceManagementException;
import org.wso2.carbon.device.mgt.common.exceptions.InvalidDeviceException;
import org.wso2.carbon.device.mgt.common.PaginationRequest;
import org.wso2.carbon.device.mgt.common.exceptions.UserNotFoundException;
import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService;
import org.wso2.carbon.device.mgt.jaxrs.beans.DeviceList;
import org.wso2.carbon.device.mgt.jaxrs.beans.ErrorResponse;
import org.wso2.carbon.device.mgt.jaxrs.service.api.admin.DeviceManagementAdminService;
@ -96,25 +114,53 @@ public class DeviceManagementAdminServiceImpl implements DeviceManagementAdminSe
@QueryParam("owner") String owner,
List<String> deviceIdentifiers){
try {
if (DeviceMgtAPIUtils.getDeviceManagementService().updateEnrollment(owner, deviceIdentifiers)){
if (DeviceMgtAPIUtils.getDeviceManagementService().updateEnrollment(owner, deviceIdentifiers)) {
String msg = "Device owner is updated successfully.";
return Response.status(Response.Status.OK).entity(msg).build();
}
String msg = "Device owner updating is failed.";
log.error(msg);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(msg).build();
} catch(InvalidDeviceException e){
} catch (InvalidDeviceException e) {
String msg = "Invalid device identifiers are found with the request.";
log.error(msg);
log.error(msg, e);
return Response.status(Response.Status.BAD_REQUEST).entity(msg).build();
}catch (DeviceManagementException e) {
} catch (DeviceManagementException e) {
String msg = "Error occurred when updating device owners.";
log.error(msg);
log.error(msg, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(msg).build();
} catch (UserNotFoundException e) {
String msg = "Couldn't found the owner in user store to update the owner of devices.";
log.error(msg);
log.error(msg, e);
return Response.status(Response.Status.BAD_REQUEST).entity(msg).build();
}
}
@DELETE
@Override
@Path("/type/{device-type}/id/{device-id}")
public Response deleteDevicePermanently(@PathParam("device-type") String deviceType,
@PathParam("device-id") String deviceId) {
DeviceManagementProviderService deviceManagementProviderService =
DeviceMgtAPIUtils.getDeviceManagementService();
try {
DeviceIdentifier deviceIdentifier = new DeviceIdentifier(deviceId, deviceType);
Device persistedDevice = deviceManagementProviderService.getDevice(deviceIdentifier, true);
if (persistedDevice == null) {
String msg = "No device found with the device type: " + deviceType +
" having the device ID: " + deviceId + " to permanently delete.";
log.error(msg);
return Response.status(Response.Status.NOT_FOUND).entity(
new ErrorResponse.ErrorResponseBuilder().setMessage(msg).build()).build();
}
boolean response = deviceManagementProviderService.deleteDevice(deviceIdentifier);
return Response.status(Response.Status.OK).entity(response).build();
} catch (DeviceManagementException e) {
String msg = "Error encountered while permanently deleting device of type : " + deviceType + " and " +
"ID : " + deviceId;
log.error(msg, e);
return Response.status(Response.Status.BAD_REQUEST).entity(
new ErrorResponse.ErrorResponseBuilder().setMessage(msg).build()).build();
}
}
}

@ -140,7 +140,8 @@ public class DeviceTypeManagementAdminServiceImpl implements DeviceTypeManagemen
@Override
@PUT
public Response updateDeviceType(String type, DeviceType deviceType) {
@Path("/{type}")
public Response updateDeviceType(@PathParam("type") String type, DeviceType deviceType) {
if (deviceType != null && deviceType.getDeviceTypeMetaDefinition() != null) {
if (deviceType.getName() == null || !deviceType.getName().equals(type)) {
return Response.status(Response.Status.BAD_REQUEST).entity("Type name mismatch. Expected: '" + type +
@ -166,7 +167,10 @@ public class DeviceTypeManagementAdminServiceImpl implements DeviceTypeManagemen
}
@Override
public Response addDeviceTypePlatformConfig(String type, PlatformConfiguration platformConfiguration) {
@POST
@Path("/{type}/configs")
public Response addDeviceTypePlatformConfig(@PathParam("type") String type,
PlatformConfiguration platformConfiguration) {
boolean isSaved;
if (platformConfiguration.getType() == null || !platformConfiguration.getType().equals(type)) {
return Response.status(Response.Status.BAD_REQUEST).entity("Type name mismatch. Expected: '" + type +

@ -481,8 +481,7 @@ public class DeviceManagementServiceImplTest {
public void testDeleteDevice() {
PowerMockito.stub(PowerMockito.method(DeviceMgtAPIUtils.class, "getDeviceManagementService"))
.toReturn(this.deviceManagementProviderService);
Response response = this.deviceManagementService.deleteDevice(TEST_DEVICE_TYPE, UUID.randomUUID().toString()
, false);
Response response = this.deviceManagementService.deleteDevice(TEST_DEVICE_TYPE, UUID.randomUUID().toString());
Assert.assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
}
@ -492,8 +491,7 @@ public class DeviceManagementServiceImplTest {
.toReturn(this.deviceManagementProviderService);
Mockito.when(this.deviceManagementProviderService
.getDevice(Mockito.any(DeviceIdentifier.class), Mockito.anyBoolean())).thenReturn(null);
Response response = this.deviceManagementService.deleteDevice(TEST_DEVICE_TYPE, UUID.randomUUID().toString()
, false);
Response response = this.deviceManagementService.deleteDevice(TEST_DEVICE_TYPE, UUID.randomUUID().toString());
Assert.assertEquals(response.getStatus(), Response.Status.NOT_FOUND.getStatusCode());
Mockito.reset(this.deviceManagementProviderService);
}
@ -504,8 +502,7 @@ public class DeviceManagementServiceImplTest {
.toReturn(this.deviceManagementProviderService);
Mockito.when(this.deviceManagementProviderService.disenrollDevice(Mockito.any(DeviceIdentifier.class)))
.thenThrow(new DeviceManagementException());
Response response = this.deviceManagementService.deleteDevice(TEST_DEVICE_TYPE, UUID.randomUUID().toString()
, false);
Response response = this.deviceManagementService.deleteDevice(TEST_DEVICE_TYPE, UUID.randomUUID().toString());
Assert.assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode());
Mockito.reset(this.deviceManagementProviderService);
}

@ -277,6 +277,17 @@ public interface DeviceDAO {
*/
List<Device> getDevices(PaginationRequest request, int tenantId) throws DeviceManagementDAOException;
/**
* This method is used to search for devices within a specific group.
*
* @param request PaginationRequest object holding the data for pagination
* @param tenantId tenant id.
* @return returns paginated list of devices.
* @throws DeviceManagementDAOException
*/
List<Device> searchDevicesInGroup(PaginationRequest request, int tenantId) throws DeviceManagementDAOException;
/**
* This method is used to retrieve all the devices of a given tenant and device type.
*
@ -498,7 +509,8 @@ public interface DeviceDAO {
* @throws DeviceManagementDAOException throws {@link DeviceManagementDAOException} if connections establishment
* fails.
*/
List<Device> getDevicesByIdentifiers(List<String> deviceIdentifiers, int tenantId) throws DeviceManagementDAOException;
List<Device> getDevicesByIdentifiers(List<String> deviceIdentifiers, int tenantId)
throws DeviceManagementDAOException;
/**
* This method is used to permanently delete the device and its related details
@ -508,4 +520,3 @@ public interface DeviceDAO {
*/
void deleteDevice(DeviceIdentifier deviceIdentifier, int tenantId) throws DeviceManagementDAOException;
}

@ -1507,8 +1507,8 @@ public abstract class AbstractDeviceDAOImpl implements DeviceDAO {
}
return devices;
} catch (SQLException e) {
throw new DeviceManagementDAOException("Error occurred while obtaining the DB connection when adding tags",
e);
throw new DeviceManagementDAOException("Error occurred while obtaining the DB connection to get devices for"
+ " given device identifiers.", e);
}
}

@ -369,7 +369,11 @@ public class EnrollmentDAOImpl implements EnrollmentDAO {
try {
Connection conn = this.getConnection();
boolean updateStatus = true;
String sql = "UPDATE DM_ENROLMENT SET OWNER = ? WHERE ID = ? AND TENANT_ID = ?";
String sql = "UPDATE "
+ "DM_ENROLMENT "
+ "SET OWNER = ? "
+ "WHERE ID = ? AND "
+ "TENANT_ID = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
if (conn.getMetaData().supportsBatchUpdates()) {
for (Device device : devices) {
@ -381,6 +385,7 @@ public class EnrollmentDAOImpl implements EnrollmentDAO {
for (int i : ps.executeBatch()) {
if (i == 0 || i == Statement.SUCCESS_NO_INFO || i == Statement.EXECUTE_FAILED) {
updateStatus = false;
break;
}
}
} else {
@ -390,14 +395,15 @@ public class EnrollmentDAOImpl implements EnrollmentDAO {
ps.setInt(3, tenantId);
if (ps.executeUpdate() == 0) {
updateStatus = false;
break;
}
}
}
}
return updateStatus;
} catch (SQLException e) {
throw new DeviceManagementDAOException("Error occurred while obtaining the DB connection when adding tags",
e);
throw new DeviceManagementDAOException("Error occurred while obtaining the DB connection to update the "
+ "owner of the device enrollment.", e);
}
}

@ -153,6 +153,137 @@ public class GenericDeviceDAOImpl extends AbstractDeviceDAOImpl {
return devices;
}
@Override
public List<Device> searchDevicesInGroup(PaginationRequest request, int tenantId)
throws DeviceManagementDAOException {
Connection conn;
PreparedStatement stmt = null;
ResultSet rs = null;
List<Device> devices = null;
int groupId = request.getGroupId();
String deviceType = request.getDeviceType();
boolean isDeviceTypeProvided = false;
String deviceName = request.getDeviceName();
boolean isDeviceNameProvided = false;
String owner = request.getOwner();
boolean isOwnerProvided = false;
String ownerPattern = request.getOwnerPattern();
boolean isOwnerPatternProvided = false;
String ownership = request.getOwnership();
boolean isOwnershipProvided = false;
String status = request.getStatus();
boolean isStatusProvided = false;
Date since = request.getSince();
boolean isSinceProvided = false;
try {
conn = this.getConnection();
String sql = "SELECT d1.DEVICE_ID, d1.DESCRIPTION, d1.NAME AS DEVICE_NAME, d1.DEVICE_TYPE, " +
"d1.DEVICE_IDENTIFICATION, e.OWNER, e.OWNERSHIP, e.STATUS, e.DATE_OF_LAST_UPDATE, " +
"e.DATE_OF_ENROLMENT, e.ID AS ENROLMENT_ID FROM DM_ENROLMENT e, " +
"(SELECT gd.DEVICE_ID, gd.DESCRIPTION, gd.NAME, gd.DEVICE_IDENTIFICATION, t.NAME AS DEVICE_TYPE " +
"FROM (SELECT d.ID AS DEVICE_ID, d.DESCRIPTION, d.NAME, d.DEVICE_IDENTIFICATION, d.DEVICE_TYPE_ID " +
"FROM DM_DEVICE d, (SELECT dgm.DEVICE_ID FROM DM_DEVICE_GROUP_MAP dgm WHERE dgm.GROUP_ID = ?) dgm1 WHERE" +
" d.ID = dgm1.DEVICE_ID AND d.TENANT_ID = ?";
//Add the query for device-name
if (deviceName != null && !deviceName.isEmpty()) {
sql = sql + " AND d.NAME LIKE ?";
isDeviceNameProvided = true;
}
sql = sql + ") gd, DM_DEVICE_TYPE t";
if (since != null) {
sql = sql + ", DM_DEVICE_DETAIL dt";
isSinceProvided = true;
}
sql = sql + " WHERE gd.DEVICE_TYPE_ID = t.ID";
//Add query for last updated timestamp
if (isSinceProvided) {
sql = sql + " AND dt.DEVICE_ID = gd.DEVICE_ID AND dt.UPDATE_TIMESTAMP > ?";
}
//Add the query for device-type
if (deviceType != null && !deviceType.isEmpty()) {
sql = sql + " AND t.NAME = ?";
isDeviceTypeProvided = true;
}
sql = sql + " ) d1 WHERE d1.DEVICE_ID = e.DEVICE_ID AND TENANT_ID = ? ";
//Add the query for ownership
if (ownership != null && !ownership.isEmpty()) {
sql = sql + " AND e.OWNERSHIP = ?";
isOwnershipProvided = true;
}
//Add the query for owner
if (owner != null && !owner.isEmpty()) {
sql = sql + " AND e.OWNER = ?";
isOwnerProvided = true;
} else if (ownerPattern != null && !ownerPattern.isEmpty()) {
sql = sql + " AND e.OWNER LIKE ?";
isOwnerPatternProvided = true;
}
//Add the query for status
if (status != null && !status.isEmpty()) {
sql = sql + " AND e.STATUS = ?";
isStatusProvided = true;
}
sql = sql + " LIMIT ?,?";
stmt = conn.prepareStatement(sql);
stmt.setInt(1, groupId);
stmt.setInt(2, tenantId);
int paramIdx = 3;
if (isDeviceNameProvided) {
stmt.setString(paramIdx++, deviceName + "%");
}
if (isSinceProvided) {
stmt.setLong(paramIdx++, since.getTime());
}
if (isDeviceTypeProvided) {
stmt.setString(paramIdx++, deviceType);
}
stmt.setInt(paramIdx++, tenantId);
if (isOwnershipProvided) {
stmt.setString(paramIdx++, ownership);
}
if (isOwnerProvided) {
stmt.setString(paramIdx++, owner);
} else if (isOwnerPatternProvided) {
stmt.setString(paramIdx++, ownerPattern + "%");
}
if (isStatusProvided) {
stmt.setString(paramIdx++, status);
}
stmt.setInt(paramIdx++, request.getStartIndex());
stmt.setInt(paramIdx, request.getRowCount());
rs = stmt.executeQuery();
devices = new ArrayList<>();
while (rs.next()) {
Device device = DeviceManagementDAOUtil.loadDevice(rs);
devices.add(device);
}
} catch (SQLException e) {
throw new DeviceManagementDAOException("Error occurred while retrieving information of" +
" devices belonging to group : " + groupId, e);
} finally {
DeviceManagementDAOUtil.cleanupResources(stmt, rs);
}
return devices;
}
@Override
public List<Device> getDevicesOfUser(PaginationRequest request, int tenantId)
throws DeviceManagementDAOException {

@ -159,6 +159,137 @@ public class OracleDeviceDAOImpl extends AbstractDeviceDAOImpl {
return devices;
}
@Override
public List<Device> searchDevicesInGroup(PaginationRequest request, int tenantId)
throws DeviceManagementDAOException {
Connection conn;
PreparedStatement stmt = null;
ResultSet rs = null;
List<Device> devices = null;
int groupId = request.getGroupId();
String deviceType = request.getDeviceType();
boolean isDeviceTypeProvided = false;
String deviceName = request.getDeviceName();
boolean isDeviceNameProvided = false;
String owner = request.getOwner();
boolean isOwnerProvided = false;
String ownerPattern = request.getOwnerPattern();
boolean isOwnerPatternProvided = false;
String ownership = request.getOwnership();
boolean isOwnershipProvided = false;
String status = request.getStatus();
boolean isStatusProvided = false;
Date since = request.getSince();
boolean isSinceProvided = false;
try {
conn = this.getConnection();
String sql = "SELECT d1.DEVICE_ID, d1.DESCRIPTION, d1.NAME AS DEVICE_NAME, d1.DEVICE_TYPE, " +
"d1.DEVICE_IDENTIFICATION, e.OWNER, e.OWNERSHIP, e.STATUS, e.DATE_OF_LAST_UPDATE, " +
"e.DATE_OF_ENROLMENT, e.ID AS ENROLMENT_ID FROM DM_ENROLMENT e, " +
"(SELECT gd.DEVICE_ID, gd.DESCRIPTION, gd.NAME, gd.DEVICE_IDENTIFICATION, t.NAME AS DEVICE_TYPE " +
"FROM (SELECT d.ID AS DEVICE_ID, d.DESCRIPTION, d.NAME, d.DEVICE_IDENTIFICATION, d.DEVICE_TYPE_ID " +
"FROM DM_DEVICE d, (SELECT dgm.DEVICE_ID FROM DM_DEVICE_GROUP_MAP dgm WHERE dgm.GROUP_ID = ?) dgm1 WHERE" +
" d.ID = dgm1.DEVICE_ID AND d.TENANT_ID = ?";
//Add the query for device-name
if (deviceName != null && !deviceName.isEmpty()) {
sql = sql + " AND d.NAME LIKE ?";
isDeviceNameProvided = true;
}
sql = sql + ") gd, DM_DEVICE_TYPE t";
if (since != null) {
sql = sql + ", DM_DEVICE_DETAIL dt";
isSinceProvided = true;
}
sql = sql + " WHERE gd.DEVICE_TYPE_ID = t.ID";
//Add query for last updated timestamp
if (isSinceProvided) {
sql = sql + " AND dt.DEVICE_ID = gd.DEVICE_ID AND dt.UPDATE_TIMESTAMP > ?";
}
//Add the query for device-type
if (deviceType != null && !deviceType.isEmpty()) {
sql = sql + " AND t.NAME = ?";
isDeviceTypeProvided = true;
}
sql = sql + " ) d1 WHERE d1.DEVICE_ID = e.DEVICE_ID AND TENANT_ID = ? ";
//Add the query for ownership
if (ownership != null && !ownership.isEmpty()) {
sql = sql + " AND e.OWNERSHIP = ?";
isOwnershipProvided = true;
}
//Add the query for owner
if (owner != null && !owner.isEmpty()) {
sql = sql + " AND e.OWNER = ?";
isOwnerProvided = true;
} else if (ownerPattern != null && !ownerPattern.isEmpty()) {
sql = sql + " AND e.OWNER LIKE ?";
isOwnerPatternProvided = true;
}
//Add the query for status
if (status != null && !status.isEmpty()) {
sql = sql + " AND e.STATUS = ?";
isStatusProvided = true;
}
sql = sql + " ORDER BY ENROLMENT_ID OFFSET ? ROWS FETCH NEXT ? ROWS ONLY";
stmt = conn.prepareStatement(sql);
stmt.setInt(1, groupId);
stmt.setInt(2, tenantId);
int paramIdx = 3;
if (isDeviceNameProvided) {
stmt.setString(paramIdx++, deviceName + "%");
}
if (isSinceProvided) {
stmt.setLong(paramIdx++, since.getTime());
}
if (isDeviceTypeProvided) {
stmt.setString(paramIdx++, deviceType);
}
stmt.setInt(paramIdx++, tenantId);
if (isOwnershipProvided) {
stmt.setString(paramIdx++, ownership);
}
if (isOwnerProvided) {
stmt.setString(paramIdx++, owner);
} else if (isOwnerPatternProvided) {
stmt.setString(paramIdx++, ownerPattern + "%");
}
if (isStatusProvided) {
stmt.setString(paramIdx++, status);
}
stmt.setInt(paramIdx++, request.getStartIndex());
stmt.setInt(paramIdx, request.getRowCount());
rs = stmt.executeQuery();
devices = new ArrayList<>();
while (rs.next()) {
Device device = DeviceManagementDAOUtil.loadDevice(rs);
devices.add(device);
}
} catch (SQLException e) {
throw new DeviceManagementDAOException("Error occurred while retrieving information of" +
" devices belonging to group : " + groupId, e);
} finally {
DeviceManagementDAOUtil.cleanupResources(stmt, rs);
}
return devices;
}
@Override
public List<Device> getDevicesOfUser(PaginationRequest request, int tenantId)
throws DeviceManagementDAOException {

@ -140,6 +140,136 @@ public class PostgreSQLDeviceDAOImpl extends AbstractDeviceDAOImpl {
return devices;
}
@Override
public List<Device> searchDevicesInGroup(PaginationRequest request, int tenantId)
throws DeviceManagementDAOException {
Connection conn;
PreparedStatement stmt = null;
ResultSet rs = null;
List<Device> devices = null;
int groupId = request.getGroupId();
String deviceType = request.getDeviceType();
boolean isDeviceTypeProvided = false;
String deviceName = request.getDeviceName();
boolean isDeviceNameProvided = false;
String owner = request.getOwner();
boolean isOwnerProvided = false;
String ownerPattern = request.getOwnerPattern();
boolean isOwnerPatternProvided = false;
String ownership = request.getOwnership();
boolean isOwnershipProvided = false;
String status = request.getStatus();
boolean isStatusProvided = false;
Date since = request.getSince();
boolean isSinceProvided = false;
try {
conn = this.getConnection();
String sql = "SELECT d1.DEVICE_ID, d1.DESCRIPTION, d1.NAME AS DEVICE_NAME, d1.DEVICE_TYPE, " +
"d1.DEVICE_IDENTIFICATION, e.OWNER, e.OWNERSHIP, e.STATUS, e.DATE_OF_LAST_UPDATE, " +
"e.DATE_OF_ENROLMENT, e.ID AS ENROLMENT_ID FROM DM_ENROLMENT e, " +
"(SELECT gd.DEVICE_ID, gd.DESCRIPTION, gd.NAME, gd.DEVICE_IDENTIFICATION, t.NAME AS DEVICE_TYPE " +
"FROM (SELECT d.ID AS DEVICE_ID, d.DESCRIPTION, d.NAME, d.DEVICE_IDENTIFICATION, d.DEVICE_TYPE_ID " +
"FROM DM_DEVICE d, (SELECT dgm.DEVICE_ID FROM DM_DEVICE_GROUP_MAP dgm WHERE dgm.GROUP_ID = ?) dgm1 WHERE" +
" d.ID = dgm1.DEVICE_ID AND d.TENANT_ID = ?";
//Add the query for device-name
if (deviceName != null && !deviceName.isEmpty()) {
sql = sql + " AND d.NAME LIKE ?";
isDeviceNameProvided = true;
}
sql = sql + ") gd, DM_DEVICE_TYPE t";
if (since != null) {
sql = sql + ", DM_DEVICE_DETAIL dt";
isSinceProvided = true;
}
sql = sql + " WHERE gd.DEVICE_TYPE_ID = t.ID";
//Add query for last updated timestamp
if (isSinceProvided) {
sql = sql + " AND dt.DEVICE_ID = gd.DEVICE_ID AND dt.UPDATE_TIMESTAMP > ?";
}
//Add the query for device-type
if (deviceType != null && !deviceType.isEmpty()) {
sql = sql + " AND t.NAME = ?";
isDeviceTypeProvided = true;
}
sql = sql + " ) d1 WHERE d1.DEVICE_ID = e.DEVICE_ID AND TENANT_ID = ? ";
//Add the query for ownership
if (ownership != null && !ownership.isEmpty()) {
sql = sql + " AND e.OWNERSHIP = ?";
isOwnershipProvided = true;
}
//Add the query for owner
if (owner != null && !owner.isEmpty()) {
sql = sql + " AND e.OWNER = ?";
isOwnerProvided = true;
} else if (ownerPattern != null && !ownerPattern.isEmpty()) {
sql = sql + " AND e.OWNER LIKE ?";
isOwnerPatternProvided = true;
}
//Add the query for status
if (status != null && !status.isEmpty()) {
sql = sql + " AND e.STATUS = ?";
isStatusProvided = true;
}
sql = sql + " LIMIT ? OFFSET ?";
stmt = conn.prepareStatement(sql);
stmt.setInt(1, groupId);
stmt.setInt(2, tenantId);
int paramIdx = 3;
if (isDeviceNameProvided) {
stmt.setString(paramIdx++, deviceName + "%");
}
if (isSinceProvided) {
stmt.setLong(paramIdx++, since.getTime());
}
if (isDeviceTypeProvided) {
stmt.setString(paramIdx++, deviceType);
}
stmt.setInt(paramIdx++, tenantId);
if (isOwnershipProvided) {
stmt.setString(paramIdx++, ownership);
}
if (isOwnerProvided) {
stmt.setString(paramIdx++, owner);
} else if (isOwnerPatternProvided) {
stmt.setString(paramIdx++, ownerPattern + "%");
}
if (isStatusProvided) {
stmt.setString(paramIdx++, status);
}
stmt.setInt(paramIdx, request.getRowCount());
stmt.setInt(paramIdx++, request.getStartIndex());
rs = stmt.executeQuery();
devices = new ArrayList<>();
while (rs.next()) {
Device device = DeviceManagementDAOUtil.loadDevice(rs);
devices.add(device);
}
} catch (SQLException e) {
throw new DeviceManagementDAOException("Error occurred while retrieving information of" +
" devices belonging to group : " + groupId, e);
} finally {
DeviceManagementDAOUtil.cleanupResources(stmt, rs);
}
return devices;
}
@Override
public List<Device> getDevicesOfUser(PaginationRequest request, int tenantId)
throws DeviceManagementDAOException {

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save