Merge branch 'appm-publisher/feature/eslint' into 'master'

Add ESLint to APPM publisher ui

Closes product-iots#294

See merge request entgra/carbon-device-mgt!421
feature/appm-store/pbac
Dharmakeerthi Lasantha 5 years ago
commit 751f2b4bb8

@ -79,6 +79,16 @@
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>lint</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run-script lint</arguments>
</configuration>
<phase>generate-resources</phase>
</execution>
<execution>
<id>prod</id>
<goals>

@ -0,0 +1,325 @@
{
"parser": "babel-eslint",
"plugins": [
"react",
"babel",
"jsx",
"prettier"
],
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"parserOptions": {
"ecmaVersion": 2016,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"settings": {
"react": {
"createClass": "createReactClass",
"pragma": "React",
"version": "16.8.6"
}
},
"env": {
"node": true,
"commonjs": true,
"browser": true,
"jasmine": true,
"es6": true
},
"globals": {
"document": true,
"console": true,
// Only for development purposes
"setTimeout": true,
"window" : true
},
"rules": {
"prettier/prettier": "error",
// Enforce the spacing around the * in generator functions.
"generator-star-spacing": [2, "after"],
// Disallow using variables outside the blocks they are defined (especially
// since only let and const are used, see "no-var").
"block-scoped-var": 2,
// Require camel case names
"camelcase": 2,
// Allow trailing commas for easy list extension. Having them does not
// impair readability, but also not required either.
"comma-dangle": 0,
// Warn about cyclomatic complexity in functions.
"complexity": 1,
// Don't warn for inconsistent naming when capturing this (not so important
// with auto-binding fat arrow functions).
"consistent-this": 0,
// Enforce curly brace conventions for all control statements.
"curly": 2,
// Don't require a default case in switch statements. Avoid being forced to
// add a bogus default when you know all possible cases are handled.
"default-case": 0,
// Encourage the use of dot notation whenever possible.
"dot-notation": 2,
// Allow mixed 'LF' and 'CRLF' as linebreaks.
"linebreak-style": 0,
// Don't enforce the maximum depth that blocks can be nested.
"max-depth": 0,
// Maximum length of a line.
"max-len": [2, 100, 2, { "ignoreStrings": true, "ignoreUrls": true}],
// Maximum depth callbacks can be nested.
"max-nested-callbacks": [2, 3],
// Don't limit the number of parameters that can be used in a function.
"max-params": 0,
// Don't limit the maximum number of statement allowed in a function.
"max-statements": 0,
// Require a capital letter for constructors, only check if all new
// operators are followed by a capital letter. Don't warn when capitalized
// functions are used without the new operator.
"new-cap": [2, {"capIsNew": false}],
// Disallow use of the Array constructor.
"no-array-constructor": 2,
// Allow use of bitwise operators.
"no-bitwise": 0,
// Disallow use of arguments.caller or arguments.callee.
"no-caller": 2,
// Disallow the catch clause parameter name being the same as a variable in
// the outer scope, to avoid confusion.
"no-catch-shadow": 2,
// Disallow assignment in conditional expressions.
"no-cond-assign": 2,
// Allow using the console API.
"no-console": 0,
// Allow using constant expressions in conditions like while (true)
"no-constant-condition": 0,
// Allow use of the continue statement.
"no-continue": 0,
// Disallow control characters in regular expressions.
"no-control-regex": 2,
// Disallow deletion of variables (deleting properties is fine).
"no-delete-var": 2,
// Disallow duplicate arguments in functions.
"no-dupe-args": 2,
// Disallow duplicate keys when creating object literals.
"no-dupe-keys": 2,
// Disallow multiple empty lines
"no-multiple-empty-lines": "error",
// Disallow a duplicate case label.
"no-duplicate-case": 2,
// Disallow else after a return in an if. The else around the second return
// here is useless:
// if (something) { return false; } else { return true; }
"no-else-return": 2,
// Disallow empty statements. This will report an error for:
// try { something(); } catch (e) {}
// but will not report it for:
// try { something(); } catch (e) { /* Silencing the error because ...*/ }
// which is a valid use case.
"no-empty": 2,
// Disallow the use of empty character classes in regular expressions.
"no-empty-character-class": 2,
// Disallow use of labels for anything other then loops and switches.
"no-labels": 2,
// Disallow use of eval(). We have other APIs to evaluate code in content.
"no-eval": 2,
// Disallow assigning to the exception in a catch block.
"no-ex-assign": 2,
// Disallow adding to native types
"no-extend-native": 2,
// Disallow unnecessary function binding.
"no-extra-bind": 2,
// Disallow double-negation boolean casts in a boolean context.
"no-extra-boolean-cast": 2,
// Allow unnecessary parentheses, as they may make the code more readable.
"no-extra-parens": 0,
// Disallow fallthrough of case statements, except if there is a comment.
"no-fallthrough": 2,
// Allow the use of leading or trailing decimal points in numeric literals.
"no-floating-decimal": 0,
// Disallow if as the only statement in an else block.
"no-lonely-if": 2,
// Disallow use of multiline strings (use template strings instead).
"no-multi-str": 2,
// Disallow reassignments of native objects.
"no-native-reassign": 2,
// Disallow nested ternary expressions, they make the code hard to read.
"no-nested-ternary": 2,
// Allow use of new operator with the require function.
"no-new-require": 0,
// Disallow use of octal literals.
"no-octal": 2,
// Allow reassignment of function parameters.
"no-param-reassign": 0,
// Allow string concatenation with __dirname and __filename (not a node env).
"no-path-concat": 0,
// Allow use of unary operators, ++ and --.
"no-plusplus": 0,
// Allow using process.env (not a node environment).
"no-process-env": 0,
// Allow using process.exit (not a node environment).
"no-process-exit": 0,
// Disallow usage of __proto__ property.
"no-proto": 2,
// Disallow declaring the same variable more than once (we use let anyway).
"no-redeclare": 2,
// Disallow multiple spaces in a regular expression literal.
"no-regex-spaces": 2,
// Allow reserved words being used as object literal keys.
"no-reserved-keys": 0,
// Don't restrict usage of specified node modules (not a node environment).
"no-restricted-modules": 0,
// Disallow use of assignment in return statement. It is preferable for a
// single line of code to have only one easily predictable effect.
"no-return-assign": 2,
// Allow use of javascript: urls.
"no-script-url": 0,
// Disallow comparisons where both sides are exactly the same.
"no-self-compare": 2,
// Disallow use of comma operator.
"no-sequences": 2,
// Warn about declaration of variables already declared in the outer scope.
// This isn't an error because it sometimes is useful to use the same name
// in a small helper function rather than having to come up with another
// random name.
// Still, making this a warning can help people avoid being confused.
"no-shadow": 0,
// Require empty line at end of file
"eol-last": "error",
// Disallow shadowing of names such as arguments.
"no-shadow-restricted-names": 2,
"no-space-before-semi": 0,
// Disallow sparse arrays, eg. let arr = [,,2].
// Array destructuring is fine though:
// for (let [, breakpointPromise] of aPromises)
"no-sparse-arrays": 2,
// Allow use of synchronous methods (not a node environment).
"no-sync": 0,
// Allow the use of ternary operators.
"no-ternary": 0,
// Don't allow spaces after end of line
"no-trailing-spaces": "error",
// Disallow throwing literals (eg. throw "error" instead of
// throw new Error("error")).
"no-throw-literal": 2,
// Disallow use of undeclared variables unless mentioned in a /*global */
// block. Note that globals from head.js are automatically imported in tests
// by the import-headjs-globals rule form the mozilla eslint plugin.
"no-undef": 2,
// Allow use of undefined variable.
"no-undefined": 0,
// Disallow the use of Boolean literals in conditional expressions.
"no-unneeded-ternary": 2,
// Disallow unreachable statements after a return, throw, continue, or break
// statement.
"no-unreachable": 2,
// Allow using variables before they are defined.
"no-unused-vars": [2, {"vars": "all", "args": "none"}],
// Disallow global and local variables that arent used, but allow unused function arguments.
"no-use-before-define": 0,
// We use var-only-at-top-level instead of no-var as we allow top level
// vars.
"no-var": 0,
// Allow using TODO/FIXME comments.
"no-warning-comments": 0,
// Disallow use of the with statement.
"no-with": 2,
// Dont require method and property shorthand syntax for object literals.
// We use this in the code a lot, but not consistently, and this seems more
// like something to check at code review time.
"object-shorthand": 0,
// Allow more than one variable declaration per function.
"one-var": 0,
// Single quotes should be used.
"quotes": [2, "single", "avoid-escape"],
// Require use of the second argument for parseInt().
"radix": 2,
// Dont require to sort variables within the same declaration block.
// Anyway, one-var is disabled.
"sort-vars": 0,
"space-after-function-name": 0,
"space-before-function-parentheses": 0,
// Disallow space before function opening parenthesis.
//"space-before-function-paren": [2, "never"],
// Disable the rule that checks if spaces inside {} and [] are there or not.
// Our code is split on conventions, and itd be nice to have 2 rules
// instead, one for [] and one for {}. So, disabling until we write them.
"space-in-brackets": 0,
// Deprecated, will be removed in 1.0.
"space-unary-word-ops": 0,
// Require a space immediately following the // in a line comment.
"spaced-comment": [2, "always"],
// Require "use strict" to be defined globally in the script.
"strict": [2, "global"],
// Disallow comparisons with the value NaN.
"use-isnan": 2,
// Warn about invalid JSDoc comments.
// Disabled for now because of https://github.com/eslint/eslint/issues/2270
// The rule fails on some jsdoc comments like in:
// devtools/client/webconsole/console-output.js
"valid-jsdoc": 0,
// Ensure that the results of typeof are compared against a valid string.
"valid-typeof": 2,
// Allow vars to be declared anywhere in the scope.
"vars-on-top": 0,
// Dont require immediate function invocation to be wrapped in parentheses.
"wrap-iife": 0,
// Don't require regex literals to be wrapped in parentheses (which
// supposedly prevent them from being mistaken for division operators).
"wrap-regex": 0,
// Require for-in loops to have an if statement.
"guard-for-in": 0,
// allow/disallow an empty newline after var statement
"newline-after-var": 0,
// disallow the use of alert, confirm, and prompt
"no-alert": 0,
// disallow the use of deprecated react changes and lifecycle methods
"react/no-deprecated": 0,
// disallow comparisons to null without a type-checking operator
"no-eq-null": 0,
// disallow overwriting functions written as function declarations
"no-func-assign": 0,
// disallow use of eval()-like methods
"no-implied-eval": 0,
// disallow function or variable declarations in nested blocks
"no-inner-declarations": 0,
// disallow invalid regular expression strings in the RegExp constructor
"no-invalid-regexp": 0,
// disallow irregular whitespace outside of strings and comments
"no-irregular-whitespace": 0,
// disallow unnecessary nested blocks
"no-lone-blocks": 0,
// disallow creation of functions within loops
"no-loop-func": 0,
// disallow use of new operator when not part of the assignment or
// comparison
"no-new": 0,
// disallow use of new operator for Function object
"no-new-func": 0,
// disallow use of the Object constructor
"no-new-object": 0,
// disallows creating new instances of String,Number, and Boolean
"no-new-wrappers": 0,
// disallow the use of object properties of the global object (Math and
// JSON) as functions
"no-obj-calls": 0,
// disallow use of octal escape sequences in string literals, such as
// var foo = "Copyright \251";
"no-octal-escape": 0,
// disallow use of undefined when initializing variables
"no-undef-init": 0,
// disallow usage of expressions in statement position
"no-unused-expressions": 0,
// disallow use of void operator
"no-void": 0,
// disallow wrapping of non-IIFE statements in parens
"no-wrap-func": 0,
// require assignment operator shorthand where possible or prohibit it
// entirely
"operator-assignment": 0,
// enforce operators to be placed before or after line breaks
"operator-linebreak": 0,
// disable chacking prop types
"react/prop-types": 0
}
}

@ -0,0 +1,5 @@
{
"singleQuote": true,
"trailingComma": "all",
"parser": "flow"
}

@ -16,14 +16,13 @@
* under the License.
*/
module.exports = function (api) {
module.exports = function(api) {
api.cache(true);
const presets = [ "@babel/preset-env",
"@babel/preset-react" ];
const plugins = ["@babel/plugin-proposal-class-properties"];
const presets = ['@babel/preset-env', '@babel/preset-react'];
const plugins = ['@babel/plugin-proposal-class-properties'];
return {
presets,
plugins
plugins,
};
};
};

@ -56,6 +56,12 @@
"body-parser": "^1.19.0",
"chai": "^4.1.2",
"css-loader": "^0.28.11",
"eslint": "^5.16.0",
"eslint-config-prettier": "4.3.0",
"eslint-plugin-babel": "5.3.0",
"eslint-plugin-jsx": "0.0.2",
"eslint-plugin-prettier": "3.1.0",
"eslint-plugin-react": "7.14.2",
"express": "^4.17.1",
"express-pino-logger": "^4.0.0",
"file-loader": "^2.0.0",
@ -74,6 +80,7 @@
"npm-run-all": "^4.1.5",
"pino-colada": "^1.4.5",
"postcss-loader": "^3.0.0",
"prettier": "1.18.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-intl": "^2.9.0",
@ -96,6 +103,7 @@
"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"
"dev2": "run-p server start",
"lint": "eslint \"src/**/*.js\""
}
}

@ -16,155 +16,168 @@
* 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, notification} from "antd";
import ConfigContext from "./context/ConfigContext";
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 { Content } = Layout;
const loadingView = (
<Layout>
<Content style={{
padding: '0 0',
paddingTop: 300,
backgroundColor: '#fff',
textAlign: 'center'
}}>
<Spin tip="Loading..."/>
</Content>
</Layout>
<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"
/>
<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: {},
};
}
constructor(props) {
super(props);
this.state = {
loading: true,
error: false,
config: {}
}
}
componentDidMount() {
this.updateFavicon();
axios.get(
window.location.origin + "/publisher/public/conf/config.json",
).then(res => {
const config = res.data;
this.checkUserLoggedIn(config);
}).catch((error) => {
this.setState({
loading: false,
error: true
})
componentDidMount() {
this.updateFavicon();
axios
.get(window.location.origin + '/publisher/public/conf/config.json')
.then(res => {
const config = res.data;
this.checkUserLoggedIn(config);
})
.catch(error => {
this.setState({
loading: false,
error: true,
});
}
});
}
getAndroidEnterpriseToken = (config) => {
axios.get(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-url?approveApps=true" +
"&searchEnabled=true&isPrivateAppsEnabled=true&isWebAppEnabled=true&isOrganizeAppPageVisible=true&isManagedConfigEnabled=true" +
"&host=" + window.location.origin,
).then(res => {
config.androidEnterpriseToken = res.data.data.token;
this.setState({
loading: false,
config: config
});
}).catch((error) => {
config.androidEnterpriseToken = null;
this.setState({
loading: false,
config: config
})
getAndroidEnterpriseToken = config => {
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/store-url?approveApps=true' +
'&searchEnabled=true&isPrivateAppsEnabled=true&isWebAppEnabled=true&isOrganizeAppPageVisible=true&isManagedConfigEnabled=true' +
'&host=' +
window.location.origin,
)
.then(res => {
config.androidEnterpriseToken = res.data.data.token;
this.setState({
loading: false,
config: config,
});
};
checkUserLoggedIn = (config) => {
axios.post(
window.location.origin + "/publisher-ui-request-handler/user",
"platform=publisher"
).then(res => {
config.user = res.data.data;
const pageURL = window.location.pathname;
const lastURLSegment = pageURL.substr(pageURL.lastIndexOf('/') + 1);
if (lastURLSegment === "login") {
window.location.href = window.location.origin + `/publisher/`;
} else {
this.getAndroidEnterpriseToken(config);
}
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
const redirectUrl = encodeURI(window.location.href);
const pageURL = window.location.pathname;
const lastURLSegment = pageURL.substr(pageURL.lastIndexOf('/') + 1);
if (lastURLSegment !== "login") {
window.location.href = window.location.origin + `/publisher/login?redirect=${redirectUrl}`;
} else {
this.getAndroidEnterpriseToken(config);
}
} else {
this.setState({
loading: false,
error: true
})
}
})
.catch(error => {
config.androidEnterpriseToken = null;
this.setState({
loading: false,
config: config,
});
};
});
};
updateFavicon = () =>{
const link = document.querySelector("link[rel*='icon']") || document.createElement('link');
link.type = 'image/x-icon';
link.rel = 'shortcut icon';
link.href = window.location.origin+'/devicemgt/public/uuf.unit.favicon/img/favicon.png';
document.getElementsByTagName('head')[0].appendChild(link);
};
checkUserLoggedIn = config => {
axios
.post(
window.location.origin + '/publisher-ui-request-handler/user',
'platform=publisher',
)
.then(res => {
config.user = res.data.data;
const pageURL = window.location.pathname;
const lastURLSegment = pageURL.substr(pageURL.lastIndexOf('/') + 1);
if (lastURLSegment === 'login') {
window.location.href = window.location.origin + '/publisher/';
} else {
this.getAndroidEnterpriseToken(config);
}
})
.catch(error => {
if (error.hasOwnProperty('response') && error.response.status === 401) {
const redirectUrl = encodeURI(window.location.href);
const pageURL = window.location.pathname;
const lastURLSegment = pageURL.substr(pageURL.lastIndexOf('/') + 1);
if (lastURLSegment !== 'login') {
window.location.href =
window.location.origin +
`/publisher/login?redirect=${redirectUrl}`;
} else {
this.getAndroidEnterpriseToken(config);
}
} else {
this.setState({
loading: false,
error: true,
});
}
});
};
updateFavicon = () => {
const link =
document.querySelector("link[rel*='icon']") ||
document.createElement('link');
link.type = 'image/x-icon';
link.rel = 'shortcut icon';
link.href =
window.location.origin +
'/devicemgt/public/uuf.unit.favicon/img/favicon.png';
document.getElementsByTagName('head')[0].appendChild(link);
};
render() {
const {loading, error} = this.state;
render() {
const { loading, error } = this.state;
const applicationView = (
<Router>
<ConfigContext.Provider value={this.state.config}>
<div>
<Switch>
<Redirect exact from="/publisher" to="/publisher/apps"/>
{this.props.routes.map((route) => (
<RouteWithSubRoutes key={route.path} {...route} />
))}
</Switch>
</div>
</ConfigContext.Provider>
</Router>
);
const applicationView = (
<Router>
<ConfigContext.Provider value={this.state.config}>
<div>
<Switch>
<Redirect exact from="/publisher" to="/publisher/apps" />
{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>
);
}
return (
<div>
{loading && loadingView}
{!loading && !error && applicationView}
{error && errorView}
</div>
);
}
}
export default App;

@ -17,21 +17,24 @@
*/
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} routes={this.props.routes}/>
)}/>
);
}
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} routes={this.props.routes} />
)}
/>
);
}
}
export default RouteWithSubRoutes;
export default RouteWithSubRoutes;

@ -16,124 +16,161 @@
* under the License.
*/
import React from "react";
import {Row, Typography, Icon, notification} from "antd";
import StarRatings from "react-star-ratings";
import "./DetailedRating.css";
import axios from "axios";
import {withConfigContext} from "../../../context/ConfigContext";
import {handleApiError} from "../../../js/Utils";
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';
import { handleApiError } from '../../../js/Utils';
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.publisher+"/admin/reviews/"+uuid+"/"+type+"-rating",
).then(res => {
if (res.status === 200) {
let detailedRating = res.data.data;
this.setState({
detailedRating
})
}
}).catch(function (error) {
handleApiError(error, "Error occurred while trying to load rating for the release.", true);
});
class DetailedRating extends React.Component {
constructor(props) {
super(props);
this.state = {
detailedRating: null,
};
}
render() {
const detailedRating = this.state.detailedRating;
componentDidMount() {
const { type, uuid } = this.props;
this.getData(type, uuid);
}
if(detailedRating ==null){
return null;
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.publisher +
'/admin/reviews/' +
uuid +
'/' +
type +
'-rating',
)
.then(res => {
if (res.status === 200) {
let detailedRating = res.data.data;
this.setState({
detailedRating,
});
}
})
.catch(function(error) {
handleApiError(
error,
'Error occurred while trying to load rating for the release.',
true,
);
});
};
const totalCount = detailedRating.noOfUsers;
const ratingVariety = detailedRating.ratingVariety;
render() {
const detailedRating = this.state.detailedRating;
const ratingArray = [];
if (detailedRating == null) {
return null;
}
for (let [key, value] of Object.entries(ratingVariety)) {
ratingArray.push(value);
}
const totalCount = detailedRating.noOfUsers;
const ratingVariety = detailedRating.ratingVariety;
const maximumRating = Math.max(...ratingArray);
const ratingArray = [];
const ratingBarPercentages = [0,0,0,0,0];
// eslint-disable-next-line no-unused-vars
for (let [key, value] of Object.entries(ratingVariety)) {
ratingArray.push(value);
}
if(maximumRating>0){
for(let i = 0; i<5; i++){
ratingBarPercentages[i] = (ratingVariety[(i+1).toString()])/maximumRating*100;
}
}
const maximumRating = Math.max(...ratingArray);
const ratingBarPercentages = [0, 0, 0, 0, 0];
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>
);
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);
export default withConfigContext(DetailedRating);

@ -16,314 +16,327 @@
* under the License.
*/
import React from "react";
import React from 'react';
import {
Card,
Col,
Row,
Typography,
Input,
Divider,
Icon,
Select,
Button,
Form,
message,
Radio,
notification, Alert
} from "antd";
import axios from "axios";
import {withConfigContext} from "../../../context/ConfigContext";
import {handleApiError} from "../../../js/Utils";
const {Option} = Select;
const {Title} = Typography;
Card,
Col,
Row,
Typography,
Divider,
Select,
Button,
Form,
Alert,
} from 'antd';
import axios from 'axios';
import { withConfigContext } from '../../../context/ConfigContext';
import { handleApiError } from '../../../js/Utils';
const { Option } = Select;
const { Title } = Typography;
class FiltersForm extends React.Component {
constructor(props) {
super(props);
this.state = {
categories: [],
tags: [],
deviceTypes: [],
forbiddenErrors: {
categories: false,
tags: false,
deviceTypes: false
}
};
}
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
for (const [key, value] of Object.entries(values)) {
if (value === undefined) {
delete values[key];
}
}
if (values.hasOwnProperty("deviceType") && values.deviceType === "ALL") {
delete values["deviceType"];
}
if (values.hasOwnProperty("appType") && values.appType === "ALL") {
delete values["appType"];
}
this.props.setFilters(values);
});
constructor(props) {
super(props);
this.state = {
categories: [],
tags: [],
deviceTypes: [],
forbiddenErrors: {
categories: false,
tags: false,
deviceTypes: false,
},
};
}
componentDidMount() {
this.getCategories();
this.getTags();
this.getDeviceTypes();
}
getCategories = () => {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications/categories"
).then(res => {
if (res.status === 200) {
let categories = JSON.parse(res.data.data);
this.setState({
categories: categories,
loading: false
});
}
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
for (const [key, value] of Object.entries(values)) {
if (value === undefined) {
delete values[key];
}
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load categories.", true);
if (error.hasOwnProperty("response") && error.response.status === 403) {
const {forbiddenErrors} = this.state;
forbiddenErrors.categories = true;
this.setState({
forbiddenErrors,
loading: false
})
} else {
this.setState({
loading: false
});
}
});
};
if (values.hasOwnProperty('deviceType') && values.deviceType === 'ALL') {
delete values.deviceType;
}
getTags = () => {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications/tags"
).then(res => {
if (res.status === 200) {
let tags = JSON.parse(res.data.data);
this.setState({
tags: tags,
loading: false,
});
}
if (values.hasOwnProperty('appType') && values.appType === 'ALL') {
delete values.appType;
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load tags.", true);
if (error.hasOwnProperty("response") && error.response.status === 403) {
const {forbiddenErrors} = this.state;
forbiddenErrors.tags = true;
this.setState({
forbiddenErrors,
loading: false
})
} else {
this.setState({
loading: false
});
}
});
};
this.props.setFilters(values);
});
};
componentDidMount() {
this.getCategories();
this.getTags();
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,
});
}
getCategories = () => {
const config = this.props.context;
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/applications/categories',
)
.then(res => {
if (res.status === 200) {
let categories = JSON.parse(res.data.data);
this.setState({
categories: categories,
loading: false,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load categories.',
true,
);
if (error.hasOwnProperty('response') && error.response.status === 403) {
const { forbiddenErrors } = this.state;
forbiddenErrors.categories = true;
this.setState({
forbiddenErrors,
loading: false,
});
} else {
this.setState({
loading: false,
});
}
});
};
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load device types.", true);
if (error.hasOwnProperty("response") && error.response.status === 403) {
const {forbiddenErrors} = this.state;
forbiddenErrors.deviceTypes = true;
this.setState({
forbiddenErrors,
loading: false
})
} else {
this.setState({
loading: false
});
}
});
};
getTags = () => {
const config = this.props.context;
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/applications/tags',
)
.then(res => {
if (res.status === 200) {
let tags = JSON.parse(res.data.data);
this.setState({
tags: tags,
loading: false,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load tags.',
true,
);
if (error.hasOwnProperty('response') && error.response.status === 403) {
const { forbiddenErrors } = this.state;
forbiddenErrors.tags = true;
this.setState({
forbiddenErrors,
loading: false,
});
} else {
this.setState({
loading: false,
});
}
});
};
render() {
const {categories, tags, deviceTypes, forbiddenErrors} = this.state;
const {getFieldDecorator} = this.props.form;
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 => {
handleApiError(
error,
'Error occurred while trying to load device types.',
true,
);
if (error.hasOwnProperty('response') && error.response.status === 403) {
const { forbiddenErrors } = this.state;
forbiddenErrors.deviceTypes = true;
this.setState({
forbiddenErrors,
loading: false,
});
} else {
this.setState({
loading: false,
});
}
});
};
return (
render() {
const { categories, tags, deviceTypes, forbiddenErrors } = this.state;
const { getFieldDecorator } = this.props.form;
<Card>
<Form labelAlign="left" layout="horizontal"
hideRequiredMark
onSubmit={this.handleSubmit}>
<Row>
<Col span={12}>
<Title level={4}>Filter</Title>
</Col>
<Col span={12}>
<Form.Item style={{
float: "right",
marginBottom: 0,
marginTop: -5
}}>
<Button
size="small"
type="primary"
htmlType="submit">
Submit
</Button>
</Form.Item>
</Col>
</Row>
{(forbiddenErrors.categories) && (
<Alert
message="You don't have permission to view categories."
type="warning"
banner
closable/>
)}
<Form.Item label="Categories">
{getFieldDecorator('categories', {
rules: [{
required: false,
message: 'Please select categories'
}],
})(
<Select
mode="multiple"
style={{width: '100%'}}
placeholder="Select a Category"
onChange={this.handleCategoryChange}>
{
categories.map(category => {
return (
<Option
key={category.categoryName}>
{category.categoryName}
</Option>
)
})
}
</Select>
)}
</Form.Item>
return (
<Card>
<Form
labelAlign="left"
layout="horizontal"
hideRequiredMark
onSubmit={this.handleSubmit}
>
<Row>
<Col span={12}>
<Title level={4}>Filter</Title>
</Col>
<Col span={12}>
<Form.Item
style={{
float: 'right',
marginBottom: 0,
marginTop: -5,
}}
>
<Button size="small" type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Col>
</Row>
{forbiddenErrors.categories && (
<Alert
message="You don't have permission to view categories."
type="warning"
banner
closable
/>
)}
<Form.Item label="Categories">
{getFieldDecorator('categories', {
rules: [
{
required: false,
message: 'Please select categories',
},
],
})(
<Select
mode="multiple"
style={{ width: '100%' }}
placeholder="Select a Category"
onChange={this.handleCategoryChange}
>
{categories.map(category => {
return (
<Option key={category.categoryName}>
{category.categoryName}
</Option>
);
})}
</Select>,
)}
</Form.Item>
{(forbiddenErrors.deviceTypes) && (
<Alert
message="You don't have permission to view device types."
type="warning"
banner
closable/>
)}
<Form.Item label="Device Type">
{getFieldDecorator('deviceType', {
rules: [{
required: false,
message: 'Please select device types'
}],
})(
<Select
style={{width: '100%'}}
placeholder="Select device types">
{
deviceTypes.map(deviceType => {
return (
<Option
key={deviceType.name}>
{deviceType.name}
</Option>
)
})
}
<Option
key="ALL">All
</Option>
</Select>
)}
</Form.Item>
{(forbiddenErrors.tags) && (
<Alert
message="You don't have permission to view tags."
type="warning"
banner
closable/>
)}
<Form.Item label="Tags">
{getFieldDecorator('tags', {
rules: [{
required: false,
message: 'Please select tags'
}],
})(
<Select
mode="multiple"
style={{width: '100%'}}
placeholder="Select tags"
>
{
tags.map(tag => {
return (
<Option
key={tag.tagName}>
{tag.tagName}
</Option>
)
})
}
</Select>
)}
</Form.Item>
{forbiddenErrors.deviceTypes && (
<Alert
message="You don't have permission to view device types."
type="warning"
banner
closable
/>
)}
<Form.Item label="Device Type">
{getFieldDecorator('deviceType', {
rules: [
{
required: false,
message: 'Please select device types',
},
],
})(
<Select
style={{ width: '100%' }}
placeholder="Select device types"
>
{deviceTypes.map(deviceType => {
return (
<Option key={deviceType.name}>{deviceType.name}</Option>
);
})}
<Option key="ALL">All</Option>
</Select>,
)}
</Form.Item>
{forbiddenErrors.tags && (
<Alert
message="You don't have permission to view tags."
type="warning"
banner
closable
/>
)}
<Form.Item label="Tags">
{getFieldDecorator('tags', {
rules: [
{
required: false,
message: 'Please select tags',
},
],
})(
<Select
mode="multiple"
style={{ width: '100%' }}
placeholder="Select tags"
>
{tags.map(tag => {
return <Option key={tag.tagName}>{tag.tagName}</Option>;
})}
</Select>,
)}
</Form.Item>
<Form.Item label="App Type">
{getFieldDecorator('appType', {})(
<Select
style={{width: '100%'}}
placeholder="Select app type"
>
<Option value="ENTERPRISE">Enterprise</Option>
<Option value="PUBLIC">Public</Option>
<Option value="WEB_CLIP">Web APP</Option>
<Option value="CUSTOM">Custom</Option>
<Option value="ALL">All</Option>
</Select>
)}
</Form.Item>
<Divider/>
</Form>
</Card>
);
}
<Form.Item label="App Type">
{getFieldDecorator('appType', {})(
<Select style={{ width: '100%' }} placeholder="Select app type">
<Option value="ENTERPRISE">Enterprise</Option>
<Option value="PUBLIC">Public</Option>
<Option value="WEB_CLIP">Web APP</Option>
<Option value="CUSTOM">Custom</Option>
<Option value="ALL">All</Option>
</Select>,
)}
</Form.Item>
<Divider />
</Form>
</Card>
);
}
}
const Filters = withConfigContext(
Form.create({ name: 'filter-apps' })(FiltersForm),
);
const Filters = withConfigContext(Form.create({name: 'filter-apps'})(FiltersForm));
export default withConfigContext(Filters);
export default withConfigContext(Filters);

@ -16,89 +16,87 @@
* under the License.
*/
import React from "react";
import {Card, Col, Row, Typography, Input, Divider, notification} from "antd";
import AppsTable from "./appsTable/AppsTable";
import Filters from "./Filters";
import AppDetailsDrawer from "./AppDetailsDrawer/AppDetailsDrawer";
import axios from "axios";
import React from 'react';
import { Card, Col, Row, Typography, Input, Divider } from 'antd';
import AppsTable from './appsTable/AppsTable';
import Filters from './Filters';
const {Title} = Typography;
const { Title } = Typography;
const Search = Input.Search;
class ListApps extends React.Component {
constructor(props) {
super(props);
this.state = {
filters: {}
};
this.appName = '';
}
setFilters = (filters) => {
if (this.appName === '' && filters.hasOwnProperty("appName")) {
delete filters["appName"];
} else {
filters.appName = this.appName;
}
this.setState({
filters
});
constructor(props) {
super(props);
this.state = {
filters: {},
};
this.appName = '';
}
setSearchText = (appName) => {
const filters = {...this.state.filters};
this.appName = appName;
if (appName === '' && filters.hasOwnProperty("appName")) {
delete filters["appName"];
} else {
filters.appName = appName;
}
this.setState({
filters
});
};
setFilters = filters => {
if (this.appName === '' && filters.hasOwnProperty('appName')) {
delete filters.appName;
} else {
filters.appName = this.appName;
}
this.setState({
filters,
});
};
onChangeSearchText = (e) => {
const filters = {...this.state.filters};
const appName = e.target.value;
if (appName === '' && filters.hasOwnProperty("appName")) {
delete filters["appName"];
this.setState({
filters
});
}
};
setSearchText = appName => {
const filters = { ...this.state.filters };
this.appName = appName;
if (appName === '' && filters.hasOwnProperty('appName')) {
delete filters.appName;
} else {
filters.appName = appName;
}
this.setState({
filters,
});
};
render() {
const {isDrawerVisible, filters} = this.state;
return (
<Card>
<Row gutter={28}>
<Col md={6}>
<Filters setFilters={this.setFilters}/>
</Col>
<Col md={18}>
<Row>
<Col span={6}>
<Title level={4}>Apps</Title>
</Col>
<Col span={18} style={{textAlign: "right"}}>
<Search
placeholder="Search by app name"
onSearch={this.setSearchText}
onChange={this.onChangeSearchText}
style={{width: 240, zIndex: 0}}
/>
</Col>
</Row>
<Divider dashed={true}/>
<AppsTable filters={filters}/>
</Col>
</Row>
</Card>
);
onChangeSearchText = e => {
const filters = { ...this.state.filters };
const appName = e.target.value;
if (appName === '' && filters.hasOwnProperty('appName')) {
delete filters.appName;
this.setState({
filters,
});
}
};
render() {
const { filters } = this.state;
return (
<Card>
<Row gutter={28}>
<Col md={6}>
<Filters setFilters={this.setFilters} />
</Col>
<Col md={18}>
<Row>
<Col span={6}>
<Title level={4}>Apps</Title>
</Col>
<Col span={18} style={{ textAlign: 'right' }}>
<Search
placeholder="Search by app name"
onSearch={this.setSearchText}
onChange={this.onChangeSearchText}
style={{ width: 240, zIndex: 0 }}
/>
</Col>
</Row>
<Divider dashed={true} />
<AppsTable filters={filters} />
</Col>
</Row>
</Card>
);
}
}
export default ListApps;

@ -16,292 +16,321 @@
* under the License.
*/
import React from "react";
import {Avatar, Table, Tag, Icon, message, notification, Col, Badge, Alert, Tooltip} from "antd";
import axios from "axios";
import React from 'react';
import { Avatar, Table, Tag, Icon, Badge, Alert, Tooltip } from 'antd';
import axios from 'axios';
import pSBC from 'shade-blend-color';
import "./AppsTable.css";
import {withConfigContext} from "../../../../context/ConfigContext";
import AppDetailsDrawer from "../AppDetailsDrawer/AppDetailsDrawer";
import {handleApiError} from "../../../../js/Utils";
import './AppsTable.css';
import { withConfigContext } from '../../../../context/ConfigContext';
import AppDetailsDrawer from '../AppDetailsDrawer/AppDetailsDrawer';
import { handleApiError } from '../../../../js/Utils';
let config = null;
const columns = [
{
title: '',
dataIndex: 'name',
render: (name, row) => {
let avatar = null;
if (row.applicationReleases.length === 0) {
const avatarLetter = name.charAt(0).toUpperCase();
avatar = (
<Avatar shape="square" size="large"
style={{
marginRight: 20,
borderRadius: "28%",
border: "1px solid #ddd",
backgroundColor: pSBC(0.50, config.theme.primaryColor)
}}>
{avatarLetter}
</Avatar>
);
} else {
const {applicationReleases} = row;
let hasPublishedRelease = false;
for (let i = 0; i < applicationReleases.length; i++) {
if (applicationReleases[i].currentStatus === "PUBLISHED") {
hasPublishedRelease = true;
break;
}
}
avatar = (hasPublishedRelease) ? (
<Badge
title="Published"
style={{backgroundColor: '#52c41a', borderRadius: "50%", color: "white"}}
count={
<Tooltip
title="Published">
<Icon
style={{
backgroundColor: '#52c41a',
borderRadius: "50%",
color: "white"
}}
type="check-circle"/>
</Tooltip>
}>
<Avatar shape="square" size="large"
style={{
borderRadius: "28%",
border: "1px solid #ddd"
}}
src={row.applicationReleases[0].iconPath}
/>
</Badge>
) : (
<Avatar shape="square" size="large"
style={{
borderRadius: "28%",
border: "1px solid #ddd"
}}
src={row.applicationReleases[0].iconPath}
/>
);
}
return (
<div>
{avatar}
<span style={{marginLeft: 20}}>{name}</span>
</div>);
{
title: '',
dataIndex: 'name',
// eslint-disable-next-line react/display-name
render: (name, row) => {
let avatar = null;
if (row.applicationReleases.length === 0) {
const avatarLetter = name.charAt(0).toUpperCase();
avatar = (
<Avatar
shape="square"
size="large"
style={{
marginRight: 20,
borderRadius: '28%',
border: '1px solid #ddd',
backgroundColor: pSBC(0.5, config.theme.primaryColor),
}}
>
{avatarLetter}
</Avatar>
);
} else {
const { applicationReleases } = row;
let hasPublishedRelease = false;
for (let i = 0; i < applicationReleases.length; i++) {
if (applicationReleases[i].currentStatus === 'PUBLISHED') {
hasPublishedRelease = true;
break;
}
}
},
{
title: 'Categories',
dataIndex: 'categories',
render: categories => (
<span>
{categories.map(category => {
return (
<Tag
style={{marginBottom: 8}}
color={pSBC(0.30, config.theme.primaryColor)}
key={category}>
{category}
</Tag>
);
})}
</span>
)
},
{
title: 'Platform',
dataIndex: 'deviceType',
render: platform => {
const defaultPlatformIcons = config.defaultPlatformIcons;
let icon = defaultPlatformIcons.default.icon;
let color = defaultPlatformIcons.default.color;
let theme = defaultPlatformIcons.default.theme;
if (defaultPlatformIcons.hasOwnProperty(platform)) {
icon = defaultPlatformIcons[platform].icon;
color = defaultPlatformIcons[platform].color;
theme = defaultPlatformIcons[platform].theme;
avatar = hasPublishedRelease ? (
<Badge
title="Published"
style={{
backgroundColor: '#52c41a',
borderRadius: '50%',
color: 'white',
}}
count={
<Tooltip title="Published">
<Icon
style={{
backgroundColor: '#52c41a',
borderRadius: '50%',
color: 'white',
}}
type="check-circle"
/>
</Tooltip>
}
return (
<span style={{fontSize: 20, color: color, textAlign: "center"}}>
<Icon type={icon} theme={theme}/>
</span>
);
}
},
{
title: 'Type',
dataIndex: 'type'
>
<Avatar
shape="square"
size="large"
style={{
borderRadius: '28%',
border: '1px solid #ddd',
}}
src={row.applicationReleases[0].iconPath}
/>
</Badge>
) : (
<Avatar
shape="square"
size="large"
style={{
borderRadius: '28%',
border: '1px solid #ddd',
}}
src={row.applicationReleases[0].iconPath}
/>
);
}
return (
<div>
{avatar}
<span style={{ marginLeft: 20 }}>{name}</span>
</div>
);
},
{
title: 'Subscription',
dataIndex: 'subMethod'
},
{
title: 'Categories',
dataIndex: 'categories',
// eslint-disable-next-line react/display-name
render: categories => (
<span>
{categories.map(category => {
return (
<Tag
style={{ marginBottom: 8 }}
color={pSBC(0.3, config.theme.primaryColor)}
key={category}
>
{category}
</Tag>
);
})}
</span>
),
},
{
title: 'Platform',
dataIndex: 'deviceType',
// eslint-disable-next-line react/display-name
render: platform => {
const defaultPlatformIcons = config.defaultPlatformIcons;
let icon = defaultPlatformIcons.default.icon;
let color = defaultPlatformIcons.default.color;
let theme = defaultPlatformIcons.default.theme;
if (defaultPlatformIcons.hasOwnProperty(platform)) {
icon = defaultPlatformIcons[platform].icon;
color = defaultPlatformIcons[platform].color;
theme = defaultPlatformIcons[platform].theme;
}
return (
<span style={{ fontSize: 20, color: color, textAlign: 'center' }}>
<Icon type={icon} theme={theme} />
</span>
);
},
},
{
title: 'Type',
dataIndex: 'type',
},
{
title: 'Subscription',
dataIndex: 'subMethod',
},
];
class AppsTable extends React.Component {
constructor(props) {
super(props);
this.state = {
pagination: {},
apps: [],
filters: {},
isDrawerVisible: false,
selectedApp: null,
selectedAppIndex: -1,
loading: false,
isForbiddenErrorVisible: false
};
config = this.props.context;
}
constructor(props) {
super(props);
this.state = {
pagination: {},
apps: [],
filters: {},
isDrawerVisible: false,
selectedApp: null,
selectedAppIndex: -1,
loading: false,
isForbiddenErrorVisible: false,
};
config = this.props.context;
}
componentDidMount() {
const {filters} = this.props;
this.setState({
filters
});
this.fetch(filters);
componentDidMount() {
const { filters } = this.props;
this.setState({
filters,
});
this.fetch(filters);
}
componentDidUpdate(prevProps, prevState, snapshot) {
const { filters } = this.props;
if (prevProps.filters !== this.props.filters) {
this.setState({
filters,
});
this.fetch(filters);
}
}
componentDidUpdate(prevProps, prevState, snapshot) {
const {filters} = this.props;
if (prevProps.filters !== this.props.filters) {
this.setState({
filters
});
this.fetch(filters);
}
}
// handler to show app drawer
showDrawer = (app, appIndex) => {
this.setState({
isDrawerVisible: true,
selectedApp: app,
selectedAppIndex: appIndex,
});
};
//handler to show app drawer
showDrawer = (app, appIndex) => {
this.setState({
isDrawerVisible: true,
selectedApp: app,
selectedAppIndex: appIndex
});
};
// handler to close the app drawer
closeDrawer = () => {
this.setState({
isDrawerVisible: false,
});
};
// handler to close the app drawer
closeDrawer = () => {
this.setState({
isDrawerVisible: false
})
};
handleTableChange = (pagination, filters, sorter) => {
const pager = { ...this.state.pagination };
pager.current = pagination.current;
handleTableChange = (pagination, filters, sorter) => {
const pager = {...this.state.pagination};
pager.current = pagination.current;
this.setState({
pagination: pager,
});
this.fetch(this.state.filters, {
results: pagination.pageSize,
page: pagination.current,
sortField: sorter.field,
sortOrder: sorter.order,
...filters,
});
};
this.setState({
pagination: pager,
});
this.fetch(this.state.filters, {
results: pagination.pageSize,
page: pagination.current,
sortField: sorter.field,
sortOrder: sorter.order,
...filters,
});
};
fetch = (filters, params = {}) => {
this.setState({loading: true});
const config = this.props.context;
if (!params.hasOwnProperty("page")) {
params.page = 1;
}
fetch = (filters, params = {}) => {
this.setState({ loading: true });
const config = this.props.context;
const data = {
offset: 10 * (params.page - 1),
limit: 10,
...filters
};
axios.post(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications",
data,
).then(res => {
if (res.status === 200) {
const data = res.data.data;
let apps = [];
if (!params.hasOwnProperty('page')) {
params.page = 1;
}
if (res.data.data.hasOwnProperty("applications")) {
apps = data.applications;
}
const pagination = {...this.state.pagination};
// Read total count from server
// pagination.total = data.totalCount;
pagination.total = data.pagination.count;
this.setState({
loading: false,
apps: apps,
pagination,
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load apps.", true);
if (error.hasOwnProperty("response") && error.response.status === 403) {
this.setState({
isForbiddenErrorVisible: true
})
}
this.setState({loading: false});
});
const data = {
offset: 10 * (params.page - 1),
limit: 10,
...filters,
};
onUpdateApp = (key, value) => {
const apps = [...this.state.apps];
apps[this.state.selectedAppIndex][key] = value;
this.setState({
apps
});
};
axios
.post(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/applications',
data,
)
.then(res => {
if (res.status === 200) {
const data = res.data.data;
let apps = [];
render() {
const {isDrawerVisible, loading} = this.state;
return (
<div>
{(this.state.isForbiddenErrorVisible) && (
<Alert
message="You don't have permission to view apps."
type="warning"
banner
closable/>
)}
<div className="apps-table">
<Table
rowKey={record => record.id}
dataSource={this.state.apps}
columns={columns}
pagination={this.state.pagination}
onChange={this.handleTableChange}
rowClassName="app-row"
loading={loading}
onRow={(record, rowIndex) => {
return {
onClick: event => {
this.showDrawer(record, rowIndex);
},
};
}}/>
<AppDetailsDrawer
visible={isDrawerVisible}
onClose={this.closeDrawer}
app={this.state.selectedApp}
onUpdateApp={this.onUpdateApp}/>
</div>
</div>
if (res.data.data.hasOwnProperty('applications')) {
apps = data.applications;
}
const pagination = { ...this.state.pagination };
// Read total count from server
// pagination.total = data.totalCount;
pagination.total = data.pagination.count;
this.setState({
loading: false,
apps: apps,
pagination,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load apps.',
true,
);
}
if (error.hasOwnProperty('response') && error.response.status === 403) {
this.setState({
isForbiddenErrorVisible: true,
});
}
this.setState({ loading: false });
});
};
onUpdateApp = (key, value) => {
const apps = [...this.state.apps];
apps[this.state.selectedAppIndex][key] = value;
this.setState({
apps,
});
};
render() {
const { isDrawerVisible, loading } = this.state;
return (
<div>
{this.state.isForbiddenErrorVisible && (
<Alert
message="You don't have permission to view apps."
type="warning"
banner
closable
/>
)}
<div className="apps-table">
<Table
rowKey={record => record.id}
dataSource={this.state.apps}
columns={columns}
pagination={this.state.pagination}
onChange={this.handleTableChange}
rowClassName="app-row"
loading={loading}
onRow={(record, rowIndex) => {
return {
onClick: event => {
this.showDrawer(record, rowIndex);
},
};
}}
/>
<AppDetailsDrawer
visible={isDrawerVisible}
onClose={this.closeDrawer}
app={this.state.selectedApp}
onUpdateApp={this.onUpdateApp}
/>
</div>
</div>
);
}
}
export default withConfigContext(AppsTable);
export default withConfigContext(AppsTable);

@ -16,158 +16,173 @@
* under the License.
*/
import React from "react";
import {Divider, Row, Col, Typography, Button, Drawer, Icon, Tooltip, Empty} from "antd";
import StarRatings from "react-star-ratings";
import Reviews from "./review/Reviews";
import "../../../App.css";
import DetailedRating from "../detailed-rating/DetailedRating";
import EditRelease from "./edit-release/EditRelease";
import {withConfigContext} from "../../../context/ConfigContext";
import NewAppUploadForm from "../../new-app/subForms/NewAppUploadForm";
import React from 'react';
import { Divider, Row, Col, Typography, Button, Icon, Tooltip } from 'antd';
import StarRatings from 'react-star-ratings';
import Reviews from './review/Reviews';
import '../../../App.css';
import DetailedRating from '../detailed-rating/DetailedRating';
import EditRelease from './edit-release/EditRelease';
import { withConfigContext } from '../../../context/ConfigContext';
const {Title, Text, Paragraph} = Typography;
const { Title, Text, Paragraph } = Typography;
class ReleaseView extends React.Component {
constructor(props) {
super(props);
this.state = {
constructor(props) {
super(props);
this.state = {};
}
}
}
componentDidMount() {
console.log("mounted: Release view");
}
render() {
const {app, release} = this.props;
const config = this.props.context;
const {lifecycle, currentLifecycleStatus} = this.props;
if (release == null || lifecycle == null) {
return null;
}
componentDidMount() {
console.log('mounted: Release view');
}
const {isAppUpdatable, isAppInstallable} = lifecycle[currentLifecycleStatus];
render() {
const { app, release } = this.props;
const config = this.props.context;
const { lifecycle, currentLifecycleStatus } = this.props;
const platform = app.deviceType;
const defaultPlatformIcons = config.defaultPlatformIcons;
let icon = defaultPlatformIcons.default.icon;
let color = defaultPlatformIcons.default.color;
let theme = defaultPlatformIcons.default.theme;
if (release == null || lifecycle == null) {
return null;
}
if (defaultPlatformIcons.hasOwnProperty(platform)) {
icon = defaultPlatformIcons[platform].icon;
color = defaultPlatformIcons[platform].color;
theme = defaultPlatformIcons[platform].theme;
}
let metaData = [];
try{
metaData = JSON.parse(release.metaData);
}catch (e) {
const { isAppUpdatable, isAppInstallable } = lifecycle[
currentLifecycleStatus
];
}
const platform = app.deviceType;
const defaultPlatformIcons = config.defaultPlatformIcons;
let icon = defaultPlatformIcons.default.icon;
let color = defaultPlatformIcons.default.color;
let theme = defaultPlatformIcons.default.theme;
return (
<div>
<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>
<StarRatings
rating={release.rating}
starRatedColor="#777"
starDimension="20px"
starSpacing="2px"
numberOfStars={5}
name='rating'
/>
<br/>
<Text>Platform : </Text>
<span style={{fontSize: 20, color: color, textAlign: "center"}}>
<Icon
type={icon}
theme={theme}
/>
</span>
<Divider type="vertical"/>
<Text>Version : {release.version}</Text><br/>
if (defaultPlatformIcons.hasOwnProperty(platform)) {
icon = defaultPlatformIcons[platform].icon;
color = defaultPlatformIcons[platform].color;
theme = defaultPlatformIcons[platform].theme;
}
let metaData = [];
try {
metaData = JSON.parse(release.metaData);
} catch (e) {
console.log(e);
}
<EditRelease
forbiddenErrors={this.props.forbiddenErrors}
isAppUpdatable={isAppUpdatable}
type={app.type}
deviceType={app.deviceType}
release={release}
updateRelease={this.props.updateRelease}
supportedOsVersions={[...this.props.supportedOsVersions]}
/>
return (
<div>
<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>
<StarRatings
rating={release.rating}
starRatedColor="#777"
starDimension="20px"
starSpacing="2px"
numberOfStars={5}
name="rating"
/>
<br />
<Text>Platform : </Text>
<span style={{ fontSize: 20, color: color, textAlign: 'center' }}>
<Icon type={icon} theme={theme} />
</span>
<Divider type="vertical" />
<Text>Version : {release.version}</Text>
<br />
</Col>
<Col xl={8} md={10} sm={24} xs={24} style={{float: "right"}}>
<div>
<Tooltip
title={isAppInstallable ? "Open this app in store" : "This release isn't in an installable state"}>
<Button
style={{float: "right"}}
htmlType="button"
type="primary"
icon="shop"
disabled={!isAppInstallable}
onClick={() => {
window.open(window.location.origin + "/store/" + app.deviceType + "/apps/" + release.uuid)
}}>
Open in store
</Button>
</Tooltip>
</div>
</Col>
</Row>
<Divider/>
<Row className="release-images">
{release.screenshots.map((screenshotUrl, index) => {
return (
<div key={index} className="release-screenshot">
<img key={screenshotUrl} src={screenshotUrl}/>
</div>
)
})}
</Row>
<Divider/>
<Paragraph type="secondary" ellipsis={{rows: 3, expandable: true}}>
{release.description}
</Paragraph>
<Divider/>
<Text>META DATA</Text>
<Row>
{
metaData.map((data, index)=>{
return (
<Col key={index} lg={8} md={6} xs={24} style={{marginTop:15}}>
<Text>{data.key}</Text><br/>
<Text type="secondary">{data.value}</Text>
</Col>
)
})
}
{(metaData.length===0) && (<Text type="secondary">No meta data available.</Text>)}
</Row>
<Divider/>
<Text>REVIEWS</Text>
<Row>
<Col lg={18}>
<DetailedRating type="release" uuid={release.uuid}/>
</Col>
</Row>
<Reviews type="release" uuid={release.uuid}/>
<EditRelease
forbiddenErrors={this.props.forbiddenErrors}
isAppUpdatable={isAppUpdatable}
type={app.type}
deviceType={app.deviceType}
release={release}
updateRelease={this.props.updateRelease}
supportedOsVersions={[...this.props.supportedOsVersions]}
/>
</Col>
<Col xl={8} md={10} sm={24} xs={24} style={{ float: 'right' }}>
<div>
<Tooltip
title={
isAppInstallable
? 'Open this app in store'
: "This release isn't in an installable state"
}
>
<Button
style={{ float: 'right' }}
htmlType="button"
type="primary"
icon="shop"
disabled={!isAppInstallable}
onClick={() => {
window.open(
window.location.origin +
'/store/' +
app.deviceType +
'/apps/' +
release.uuid,
);
}}
>
Open in store
</Button>
</Tooltip>
</div>
</Col>
</Row>
<Divider />
<Row className="release-images">
{release.screenshots.map((screenshotUrl, index) => {
return (
<div key={index} className="release-screenshot">
<img key={screenshotUrl} src={screenshotUrl} />
</div>
</div>
);
}
);
})}
</Row>
<Divider />
<Paragraph type="secondary" ellipsis={{ rows: 3, expandable: true }}>
{release.description}
</Paragraph>
<Divider />
<Text>META DATA</Text>
<Row>
{metaData.map((data, index) => {
return (
<Col
key={index}
lg={8}
md={6}
xs={24}
style={{ marginTop: 15 }}
>
<Text>{data.key}</Text>
<br />
<Text type="secondary">{data.value}</Text>
</Col>
);
})}
{metaData.length === 0 && (
<Text type="secondary">No meta data available.</Text>
)}
</Row>
<Divider />
<Text>REVIEWS</Text>
<Row>
<Col lg={18}>
<DetailedRating type="release" uuid={release.uuid} />
</Col>
</Row>
<Reviews type="release" uuid={release.uuid} />
</div>
</div>
);
}
}
export default withConfigContext(ReleaseView);

@ -16,201 +16,231 @@
* under the License.
*/
import React from "react";
import {Typography, Tag, Divider, Select, Button, Modal, message, notification, Collapse} from "antd";
import axios from "axios";
import React from 'react';
import {
Typography,
Tag,
Divider,
Select,
Button,
Modal,
notification,
} from 'antd';
import axios from 'axios';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import './LifeCycle.css';
import LifeCycleDetailsModal from "./lifeCycleDetailsModal/lifeCycleDetailsModal";
import {withConfigContext} from "../../../../context/ConfigContext";
import {handleApiError} from "../../../../js/Utils";
import LifeCycleDetailsModal from './lifeCycleDetailsModal/lifeCycleDetailsModal';
import { withConfigContext } from '../../../../context/ConfigContext';
import { handleApiError } from '../../../../js/Utils';
const {Text, Title, Paragraph} = Typography;
const {Option} = Select;
const { Text, Title, Paragraph } = Typography;
const { Option } = Select;
const modules = {
toolbar: [
[{'header': [1, 2, false]}],
['bold', 'italic', 'underline', 'strike', 'blockquote', 'code-block'],
[{'list': 'ordered'}, {'list': 'bullet'}],
['link', 'image']
],
toolbar: [
[{ header: [1, 2, false] }],
['bold', 'italic', 'underline', 'strike', 'blockquote', 'code-block'],
[{ list: 'ordered' }, { list: 'bullet' }],
['link', 'image'],
],
};
const formats = [
'header',
'bold', 'italic', 'underline', 'strike', 'blockquote', 'code-block',
'list', 'bullet',
'link', 'image'
'header',
'bold',
'italic',
'underline',
'strike',
'blockquote',
'code-block',
'list',
'bullet',
'link',
'image',
];
class LifeCycle extends React.Component {
constructor(props) {
super(props);
this.state = {
currentStatus: props.currentStatus,
selectedStatus: null,
reasonText: '',
isReasonModalVisible: false,
isConfirmButtonLoading: false,
};
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (
prevProps.currentStatus !== this.props.currentStatus ||
prevProps.uuid !== this.props.uuid
) {
this.setState({
currentStatus: this.props.currentStatus,
});
}
}
handleChange = value => {
this.setState({ reasonText: value });
};
handleSelectChange = value => {
this.setState({ selectedStatus: value });
};
showReasonModal = () => {
this.setState({
isReasonModalVisible: true,
});
};
closeReasonModal = () => {
this.setState({
isReasonModalVisible: false,
});
};
addLifeCycle = () => {
const config = this.props.context;
const { selectedStatus, reasonText } = this.state;
const { uuid } = this.props;
const data = {
action: selectedStatus,
reason: reasonText,
};
constructor(props) {
super(props);
this.state = {
currentStatus: props.currentStatus,
this.setState({
isConfirmButtonLoading: true,
});
axios
.post(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/applications/life-cycle/' +
uuid,
data,
)
.then(res => {
if (res.status === 201) {
this.setState({
isReasonModalVisible: false,
isConfirmButtonLoading: false,
currentStatus: selectedStatus,
selectedStatus: null,
reasonText: '',
isReasonModalVisible: false,
isConfirmButtonLoading: false
}
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (prevProps.currentStatus !== this.props.currentStatus || prevProps.uuid !== this.props.uuid) {
this.setState({
currentStatus: this.props.currentStatus
});
});
this.props.changeCurrentLifecycleStatus(selectedStatus);
notification.success({
message: 'Done!',
description: 'Lifecycle state updated successfully!',
});
}
}
handleChange = (value) => {
this.setState({reasonText: value})
};
handleSelectChange = (value) => {
this.setState({selectedStatus: value})
};
showReasonModal = () => {
this.setState({
isReasonModalVisible: true
});
};
closeReasonModal = () => {
})
.catch(error => {
handleApiError(error, 'Error occurred while trying to add lifecycle');
this.setState({
isReasonModalVisible: false
});
};
addLifeCycle = () => {
const config = this.props.context;
const {selectedStatus, reasonText} = this.state;
const {uuid} = this.props;
const data = {
action: selectedStatus,
reason: reasonText
};
this.setState({
isConfirmButtonLoading: true,
});
axios.post(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications/life-cycle/" + uuid,
data
).then(res => {
if (res.status === 201) {
this.setState({
isReasonModalVisible: false,
isConfirmButtonLoading: false,
currentStatus: selectedStatus,
selectedStatus: null,
reasonText: ''
});
this.props.changeCurrentLifecycleStatus(selectedStatus);
notification["success"]({
message: "Done!",
description:
"Lifecycle state updated successfully!",
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to add lifecycle");
this.setState({
isConfirmButtonLoading: false
});
isConfirmButtonLoading: false,
});
};
render() {
const {currentStatus, selectedStatus, isConfirmButtonLoading} = this.state;
const {lifecycle} = this.props;
const selectedValue = selectedStatus == null ? [] : selectedStatus;
let proceedingStates = [];
if (lifecycle !== null && (lifecycle.hasOwnProperty(currentStatus)) && lifecycle[currentStatus].hasOwnProperty("proceedingStates")) {
proceedingStates = lifecycle[currentStatus].proceedingStates;
}
return (
<div>
<Title level={4}>Manage Lifecycle</Title>
<Divider/>
<Paragraph>
Ensure that your security policies are not violated by the application. Have a thorough review and
approval process before directly publishing it to your app store. You can easily transition from one
state to another. <br/>Note: Change State To displays only the next states allowed from the
current state
</Paragraph>
{lifecycle !== null && (<LifeCycleDetailsModal lifecycle={lifecycle}/>)}
<Divider dashed={true}/>
<Text strong={true}>Current State: </Text> <Tag color="blue">{currentStatus}</Tag><br/><br/>
<Text>Change State to: </Text>
<Select
placeholder="Select state"
style={{width: 120}}
size="small"
onChange={this.handleSelectChange}
value={selectedValue}
showSearch={true}
>
{proceedingStates.map(lifecycleState => {
return (
<Option
key={lifecycleState}
value={lifecycleState}>
{lifecycleState}
</Option>
)
})
}
</Select>
<Button
style={{marginLeft: 10}}
size="small"
type="primary"
htmlType="button"
onClick={this.showReasonModal}
loading={isConfirmButtonLoading}
disabled={selectedStatus == null}>
Change
</Button>
<Divider/>
<Modal
title="Confirm changing lifecycle state"
visible={this.state.isReasonModalVisible}
onOk={this.addLifeCycle}
onCancel={this.closeReasonModal}
okText="Confirm">
<Text>
You are going to change the lifecycle state from,<br/>
<Tag color="blue">{currentStatus}</Tag>to <Tag
color="blue">{selectedStatus}</Tag>
</Text>
<br/><br/>
<ReactQuill
theme="snow"
value={this.state.reasonText}
onChange={this.handleChange}
modules={modules}
formats={formats}
placeholder="Leave a comment (optional)"
/>
</Modal>
</div>
);
});
};
render() {
const {
currentStatus,
selectedStatus,
isConfirmButtonLoading,
} = this.state;
const { lifecycle } = this.props;
const selectedValue = selectedStatus == null ? [] : selectedStatus;
let proceedingStates = [];
if (
lifecycle !== null &&
lifecycle.hasOwnProperty(currentStatus) &&
lifecycle[currentStatus].hasOwnProperty('proceedingStates')
) {
proceedingStates = lifecycle[currentStatus].proceedingStates;
}
return (
<div>
<Title level={4}>Manage Lifecycle</Title>
<Divider />
<Paragraph>
Ensure that your security policies are not violated by the
application. Have a thorough review and approval process before
directly publishing it to your app store. You can easily transition
from one state to another. <br />
Note: Change State To displays only the next states allowed from the
current state
</Paragraph>
{lifecycle !== null && <LifeCycleDetailsModal lifecycle={lifecycle} />}
<Divider dashed={true} />
<Text strong={true}>Current State: </Text>{' '}
<Tag color="blue">{currentStatus}</Tag>
<br />
<br />
<Text>Change State to: </Text>
<Select
placeholder="Select state"
style={{ width: 120 }}
size="small"
onChange={this.handleSelectChange}
value={selectedValue}
showSearch={true}
>
{proceedingStates.map(lifecycleState => {
return (
<Option key={lifecycleState} value={lifecycleState}>
{lifecycleState}
</Option>
);
})}
</Select>
<Button
style={{ marginLeft: 10 }}
size="small"
type="primary"
htmlType="button"
onClick={this.showReasonModal}
loading={isConfirmButtonLoading}
disabled={selectedStatus == null}
>
Change
</Button>
<Divider />
<Modal
title="Confirm changing lifecycle state"
visible={this.state.isReasonModalVisible}
onOk={this.addLifeCycle}
onCancel={this.closeReasonModal}
okText="Confirm"
>
<Text>
You are going to change the lifecycle state from,
<br />
<Tag color="blue">{currentStatus}</Tag>to{' '}
<Tag color="blue">{selectedStatus}</Tag>
</Text>
<br />
<br />
<ReactQuill
theme="snow"
value={this.state.reasonText}
onChange={this.handleChange}
modules={modules}
formats={formats}
placeholder="Leave a comment (optional)"
/>
</Modal>
</div>
);
}
}
export default withConfigContext(LifeCycle);

@ -16,99 +16,93 @@
* under the License.
*/
import React from "react";
import {Modal, Button, Tag, List, Typography} from 'antd';
import pSBC from "shade-blend-color";
import {withConfigContext} from "../../../../../context/ConfigContext";
import React from 'react';
import { Modal, Button, Tag, List, Typography } from 'antd';
import pSBC from 'shade-blend-color';
import { withConfigContext } from '../../../../../context/ConfigContext';
const {Text} = Typography;
const { Text } = Typography;
class LifeCycleDetailsModal extends React.Component {
constructor(props) {
super(props);
this.state = { visible: false };
}
constructor(props) {
super(props);
this.state = {visible: false};
}
showModal = () => {
this.setState({
visible: true,
});
};
showModal = () => {
this.setState({
visible: true,
});
};
handleCancel = e => {
this.setState({
visible: false,
});
};
handleCancel = e => {
this.setState({
visible: false,
});
};
render() {
const config = this.props.context;
const lifeCycleConfig = config.lifecycle;
const { lifecycle } = this.props;
return (
<div>
<Button size="small" icon="question-circle" onClick={this.showModal}>
Learn more
</Button>
<Modal
title="Lifecycle"
visible={this.state.visible}
footer={null}
onCancel={this.handleCancel}
>
<List
itemLayout="horizontal"
dataSource={Object.keys(lifecycle)}
renderItem={lifecycleState => {
let text = '';
let footerText = '';
let nextProceedingStates = [];
render() {
const config = this.props.context;
const lifeCycleConfig = config.lifecycle;
const {lifecycle} = this.props;
return (
<div>
<Button
size="small"
icon="question-circle"
onClick={this.showModal}
>
Learn more
</Button>
<Modal
title="Lifecycle"
visible={this.state.visible}
footer={null}
onCancel={this.handleCancel}
>
if (lifeCycleConfig.hasOwnProperty(lifecycleState)) {
text = lifeCycleConfig[lifecycleState].text;
}
if (
lifecycle[lifecycleState].hasOwnProperty('proceedingStates')
) {
nextProceedingStates =
lifecycle[lifecycleState].proceedingStates;
footerText =
'You can only proceed to one of the following states:';
}
<List
itemLayout="horizontal"
dataSource={Object.keys(lifecycle)}
renderItem={lifecycleState => {
let text = "";
let footerText = "";
let nextProceedingStates = [];
if (lifeCycleConfig.hasOwnProperty(lifecycleState)) {
text = lifeCycleConfig[lifecycleState].text;
}
if (lifecycle[lifecycleState].hasOwnProperty("proceedingStates")) {
nextProceedingStates = lifecycle[lifecycleState].proceedingStates;
footerText = "You can only proceed to one of the following states:"
}
return (
<List.Item>
<List.Item.Meta
title={lifecycleState}
/>
{text}
<br/>
<Text type="secondary">{footerText}</Text>
<div>
{
nextProceedingStates.map(lifecycleState => {
return (
<Tag
key={lifecycleState}
style={{margin: 5}}
color={pSBC(0.30, config.theme.primaryColor)}
>
{lifecycleState}
</Tag>
)
})
}
</div>
</List.Item>
)
}}
/>
</Modal>
</div>
);
}
return (
<List.Item>
<List.Item.Meta title={lifecycleState} />
{text}
<br />
<Text type="secondary">{footerText}</Text>
<div>
{nextProceedingStates.map(lifecycleState => {
return (
<Tag
key={lifecycleState}
style={{ margin: 5 }}
color={pSBC(0.3, config.theme.primaryColor)}
>
{lifecycleState}
</Tag>
);
})}
</div>
</List.Item>
);
}}
/>
</Modal>
</div>
);
}
}
export default withConfigContext(LifeCycleDetailsModal);

@ -16,147 +16,166 @@
* under the License.
*/
import React from "react";
import {List, message, Avatar, Spin, Button, notification, Alert} from 'antd';
import "./Reviews.css";
import React from 'react';
import { List, Spin, Button, Alert } from 'antd';
import './Reviews.css';
import InfiniteScroll from 'react-infinite-scroller';
import SingleReview from "./SingleReview";
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
import {handleApiError} from "../../../../js/Utils";
import SingleReview from './SingleReview';
import axios from 'axios';
import { withConfigContext } from '../../../../context/ConfigContext';
import { handleApiError } from '../../../../js/Utils';
const limit = 5;
class Reviews extends React.Component {
state = {
data: [],
loading: false,
hasMore: false,
loadMore: false,
forbiddenErrors: {
reviews: false
}
};
state = {
data: [],
loading: false,
hasMore: false,
loadMore: false,
forbiddenErrors: {
reviews: false,
},
};
componentDidMount() {
this.fetchData(0, limit, res => {
this.setState({
data: res,
});
});
}
componentDidMount() {
this.fetchData(0, limit, res => {
this.setState({
data: res,
});
});
}
fetchData = (offset, limit, callback) => {
const config = this.props.context;
fetchData = (offset, limit, callback) => {
const config = this.props.context;
const { uuid, type } = this.props;
this.setState({
loading: true,
});
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/admin/reviews/' +
type +
'/' +
uuid,
)
.then(res => {
if (res.status === 200) {
let reviews = res.data.data.data;
callback(reviews);
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load reviews.',
true,
);
if (error.hasOwnProperty('response') && error.response.status === 403) {
const { forbiddenErrors } = this.state;
forbiddenErrors.reviews = true;
this.setState({
forbiddenErrors,
loading: false,
});
} else {
this.setState({
loading: false,
});
}
});
};
const {uuid, type} = this.props;
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({
loading: true
});
axios.get(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
"/admin/reviews/" + type + "/" + uuid
).then(res => {
if (res.status === 200) {
let reviews = res.data.data.data;
callback(reviews);
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load reviews.", true);
if (error.hasOwnProperty("response") && error.response.status === 403) {
const {forbiddenErrors} = this.state;
forbiddenErrors.reviews = true;
this.setState({
forbiddenErrors,
loading: false
})
} else {
this.setState({
loading: false
});
}
data,
loading: false,
});
};
handleInfiniteOnLoad = (count) => {
const offset = count * limit;
let data = this.state.data;
} else {
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
});
}
hasMore: false,
loading: false,
});
};
}
});
};
enableLoading = () => {
this.setState({
hasMore: true,
loadMore: true
});
};
enableLoading = () => {
this.setState({
hasMore: true,
loadMore: true,
});
};
render() {
return (
<div>
{(this.state.forbiddenErrors.reviews) && (
<Alert
message="You don't have permission to view reviews."
type="warning"
banner
closable/>
)}
<div className="demo-infinite-container">
<InfiniteScroll
initialLoad={false}
pageStart={0}
loadMore={this.handleInfiniteOnLoad}
hasMore={!this.state.loading && this.state.hasMore}
useWindow={true}>
<List
dataSource={this.state.data}
renderItem={item => (
<List.Item key={item.id}>
<SingleReview review={item}/>
</List.Item>
)}>
{this.state.loading && this.state.hasMore && (
<div className="demo-loading-container">
<Spin/>
</div>
)}
</List>
</InfiniteScroll>
{!this.state.loadMore && (this.state.data.length >= limit) && (<div style={{textAlign: "center"}}>
<Button type="dashed" htmlType="button" onClick={this.enableLoading}>Read All Reviews</Button>
</div>)}
render() {
return (
<div>
{this.state.forbiddenErrors.reviews && (
<Alert
message="You don't have permission to view reviews."
type="warning"
banner
closable
/>
)}
<div className="demo-infinite-container">
<InfiniteScroll
initialLoad={false}
pageStart={0}
loadMore={this.handleInfiniteOnLoad}
hasMore={!this.state.loading && this.state.hasMore}
useWindow={true}
>
<List
dataSource={this.state.data}
renderItem={item => (
<List.Item key={item.id}>
<SingleReview review={item} />
</List.Item>
)}
>
{this.state.loading && this.state.hasMore && (
<div className="demo-loading-container">
<Spin />
</div>
)}
</List>
</InfiniteScroll>
{!this.state.loadMore && this.state.data.length >= limit && (
<div style={{ textAlign: 'center' }}>
<Button
type="dashed"
htmlType="button"
onClick={this.enableLoading}
>
Read All Reviews
</Button>
</div>
);
}
)}
</div>
</div>
);
}
}
export default withConfigContext(Reviews);

@ -16,49 +16,69 @@
* under the License.
*/
import React from "react";
import {Avatar} from "antd";
import {List,Typography} from "antd";
import StarRatings from "react-star-ratings";
import React from 'react';
import { Avatar } from 'antd';
import { List, Typography } from 'antd';
import StarRatings from 'react-star-ratings';
const {Text, Paragraph} = Typography;
const colorList = ['#f0932b','#badc58','#6ab04c','#eb4d4b','#0abde3', '#9b59b6','#3498db','#22a6b3'];
const { Text, Paragraph } = Typography;
const colorList = [
'#f0932b',
'#badc58',
'#6ab04c',
'#eb4d4b',
'#0abde3',
'#9b59b6',
'#3498db',
'#22a6b3',
];
class SingleReview extends React.Component {
render() {
const review = this.props.review;
const randomColor = colorList[Math.floor(Math.random() * colorList.length)];
const avatarLetter = review.username.charAt(0).toUpperCase();
const content = (
<div style={{ marginTop: -5 }}>
<StarRatings
rating={review.rating}
starRatedColor="#777"
starDimension="12px"
starSpacing="2px"
numberOfStars={5}
name="rating"
/>
<Text style={{ fontSize: 12, color: '#aaa' }} type="secondary">
{' '}
{review.createdAt}
</Text>
<br />
<Paragraph
ellipsis={{ rows: 3, expandable: true }}
style={{ color: '#777' }}
>
{review.content}
</Paragraph>
</div>
);
render() {
const review = this.props.review;
const randomColor = colorList[Math.floor(Math.random() * (colorList.length))];
const avatarLetter = review.username.charAt(0).toUpperCase();
const content = (
<div style={{marginTop: -5}}>
<StarRatings
rating={review.rating}
starRatedColor="#777"
starDimension = "12px"
starSpacing = "2px"
numberOfStars={5}
name='rating'
/>
<Text style={{fontSize: 12, color: "#aaa"}} type="secondary"> {review.createdAt}</Text><br/>
<Paragraph ellipsis={{ rows: 3, expandable: true }} style={{color: "#777"}}>{review.content}</Paragraph>
</div>
);
return (
<div>
<List.Item.Meta
avatar={
<Avatar style={{ backgroundColor: randomColor, verticalAlign: 'middle' }} size="large">
{avatarLetter}
</Avatar>
}
title={review.username}
description={content}
/>
</div>
);
}
return (
<div>
<List.Item.Meta
avatar={
<Avatar
style={{ backgroundColor: randomColor, verticalAlign: 'middle' }}
size="large"
>
{avatarLetter}
</Avatar>
}
title={review.username}
description={content}
/>
</div>
);
}
}
export default SingleReview;
export default SingleReview;

@ -16,109 +16,112 @@
* under the License.
*/
import React from "react";
import {Button, Divider, Form, Input, message, Modal, notification, Spin} from "antd";
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
import {withRouter} from "react-router";
import {handleApiError} from "../../../../js/Utils";
import React from 'react';
import { Button, Divider, Input, Modal, notification, Spin } from 'antd';
import axios from 'axios';
import { withConfigContext } from '../../../../context/ConfigContext';
import { withRouter } from 'react-router';
import { handleApiError } from '../../../../js/Utils';
class AddNewPage extends React.Component {
state = {
visible: false,
pageName: '',
};
state = {
visible: false,
pageName: ''
};
showModal = () => {
this.setState({
visible: true,
loading: false,
});
};
showModal = () => {
this.setState({
visible: true,
loading: false
});
};
handleCancel = e => {
this.setState({
visible: false,
});
};
handlePageName = e => {
this.setState({
pageName: e.target.value,
});
};
handleCancel = e => {
this.setState({
visible: false,
});
};
createNewPage = () => {
const config = this.props.context;
this.setState({ loading: true });
handlePageName = (e) => {
this.setState({
pageName: e.target.value,
});
};
axios
.post(
window.location.origin +
config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/store-layout/page',
{
locale: 'en',
pageName: this.state.pageName,
},
)
.then(res => {
if (res.status === 200) {
const { pageId, pageName } = res.data.data;
createNewPage = () => {
const config = this.props.context;
this.setState({loading: true});
notification.success({
message: 'Saved!',
description: 'Page created successfully!',
});
axios.post(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/page",
{
"locale": "en",
"pageName": this.state.pageName
}
).then(res => {
if (res.status === 200) {
this.setState({ loading: false });
const {pageId, pageName} = res.data.data;
notification["success"]({
message: 'Saved!',
description: 'Page created successfully!'
});
this.setState({loading: false});
this.props.history.push(`/publisher/manage/android-enterprise/pages/${pageName}/${pageId}`);
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to update the cluster.");
this.setState({loading: false});
});
};
this.props.history.push(
`/publisher/manage/android-enterprise/pages/${pageName}/${pageId}`,
);
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to update the cluster.',
);
this.setState({ loading: false });
});
};
render() {
return (
<div style={{marginTop: 24, marginBottom: 24}}>
<Button
type="dashed"
onClick={this.showModal}>
Add new page
</Button>
<Modal
title="Add new page"
visible={this.state.visible}
onOk={this.createNewPage}
onCancel={this.handleCancel}
okText="Create Page"
footer={null}
>
<Spin spinning={this.state.loading}>
<p>Choose a name for the page</p>
<Input onChange={this.handlePageName}/>
<Divider/>
<div>
<Button
onClick={this.handleCancel}>
Cancel
</Button>
<Divider type="vertical"/>
<Button
onClick={this.createNewPage}
htmlType="button" type="primary"
disabled={this.state.pageName.length === 0}>
Create Page
</Button>
</div>
</Spin>
</Modal>
render() {
return (
<div style={{ marginTop: 24, marginBottom: 24 }}>
<Button type="dashed" onClick={this.showModal}>
Add new page
</Button>
<Modal
title="Add new page"
visible={this.state.visible}
onOk={this.createNewPage}
onCancel={this.handleCancel}
okText="Create Page"
footer={null}
>
<Spin spinning={this.state.loading}>
<p>Choose a name for the page</p>
<Input onChange={this.handlePageName} />
<Divider />
<div>
<Button onClick={this.handleCancel}>Cancel</Button>
<Divider type="vertical" />
<Button
onClick={this.createNewPage}
htmlType="button"
type="primary"
disabled={this.state.pageName.length === 0}
>
Create Page
</Button>
</div>
);
}
</Spin>
</Modal>
</div>
);
}
}
export default withConfigContext(withRouter(AddNewPage));
export default withConfigContext(withRouter(AddNewPage));

@ -16,64 +16,68 @@
* under the License.
*/
import React from "react";
import {Modal, Button} from "antd";
import {withConfigContext} from "../../../context/ConfigContext";
import React from 'react';
import { Modal, Button } from 'antd';
import { withConfigContext } from '../../../context/ConfigContext';
class GooglePlayIframe extends React.Component {
constructor(props) {
super(props);
this.config = this.props.context;
constructor(props) {
super(props);
this.config = this.props.context;
this.state = {
visible: false
};
}
showModal = () => {
this.setState({
visible: true,
});
this.state = {
visible: false,
};
}
handleOk = e => {
this.setState({
visible: false,
});
};
showModal = () => {
this.setState({
visible: true,
});
};
handleCancel = e => {
this.setState({
visible: false,
});
};
handleOk = e => {
this.setState({
visible: false,
});
};
handleCancel = e => {
this.setState({
visible: false,
});
};
render() {
return (
<div style={{display: "inline-block", padding: 4}}>
<Button type="primary" onClick={this.showModal}>
Approve Applications
</Button>
<Modal
title={null}
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
width = {740}
footer={null}>
<iframe
style={{
height: 720,
border: 0,
width: "100%"
}}
src={"https://play.google.com/work/embedded/search?token=" + this.config.androidEnterpriseToken +
"&mode=APPROVE&showsearchbox=TRUE"}
/>
</Modal>
</div>
);
}
render() {
return (
<div style={{ display: 'inline-block', padding: 4 }}>
<Button type="primary" onClick={this.showModal}>
Approve Applications
</Button>
<Modal
title={null}
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
width={740}
footer={null}
>
<iframe
style={{
height: 720,
border: 0,
width: '100%',
}}
src={
'https://play.google.com/work/embedded/search?token=' +
this.config.androidEnterpriseToken +
'&mode=APPROVE&showsearchbox=TRUE'
}
/>
</Modal>
</div>
);
}
}
export default withConfigContext(GooglePlayIframe);
export default withConfigContext(GooglePlayIframe);

@ -16,183 +16,215 @@
* under the License.
*/
import React from "react";
import {Button, message, Modal, notification, Spin} from "antd";
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
import {handleApiError} from "../../../../js/Utils";
// import gapi from 'gapi-client';
import React from 'react';
import { Button, Modal, notification, Spin } from 'antd';
import axios from 'axios';
import { withConfigContext } from '../../../../context/ConfigContext';
import { handleApiError } from '../../../../js/Utils';
class ManagedConfigurationsIframe extends React.Component {
constructor(props) {
super(props);
this.config = this.props.context;
this.state = {
visible: false,
loading: false
};
}
showModal = () => {
this.getMcm();
this.setState({
visible: true,
});
constructor(props) {
super(props);
this.config = this.props.context;
this.state = {
visible: false,
loading: false,
};
handleOk = e => {
this.setState({
visible: false,
});
}
showModal = () => {
this.getMcm();
this.setState({
visible: true,
});
};
handleOk = e => {
this.setState({
visible: false,
});
};
handleCancel = e => {
this.setState({
visible: false,
});
};
getMcm = () => {
const { packageName } = this.props;
this.setState({ loading: true });
// send request to the invoker
axios
.get(
window.location.origin +
this.config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/managed-configs/package/' +
packageName,
)
.then(res => {
if (res.status === 200) {
let mcmId = null;
if (res.data.hasOwnProperty('data')) {
mcmId = res.data.data.mcmId;
}
this.loadIframe(mcmId);
this.setState({ loading: false });
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load configurations.',
);
this.setState({ loading: false, visible: false });
});
};
loadIframe = mcmId => {
const { packageName } = this.props;
let method = 'post';
// eslint-disable-next-line no-undef
gapi.load('gapi.iframes', () => {
const parameters = {
token: this.config.androidEnterpriseToken,
packageName: packageName,
};
if (mcmId != null) {
parameters.mcmId = mcmId;
parameters.canDelete = true;
method = 'put';
}
const queryString = Object.keys(parameters)
.map(key => key + '=' + parameters[key])
.join('&');
var options = {
url: 'https://play.google.com/managed/mcm?' + queryString,
where: document.getElementById('manage-config-iframe-container'),
attributes: { style: 'height:720px', scrolling: 'yes' },
};
// eslint-disable-next-line no-undef
var iframe = gapi.iframes.getContext().openChild(options);
iframe.register(
'onconfigupdated',
event => {
this.updateConfig(method, event);
},
// eslint-disable-next-line no-undef
gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER,
);
iframe.register(
'onconfigdeleted',
event => {
this.deleteConfig(event);
},
// eslint-disable-next-line no-undef
gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER,
);
});
};
updateConfig = (method, event) => {
const { packageName } = this.props;
this.setState({ loading: true });
const data = {
mcmId: event.mcmId,
profileName: event.name,
packageName,
};
handleCancel = e => {
this.setState({
// send request to the invoker
axios({
method,
url:
window.location.origin +
this.config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/managed-configs',
data,
})
.then(res => {
if (res.status === 200 || res.status === 201) {
notification.success({
message: 'Saved!',
description: 'Configuration Profile updated Successfully',
});
this.setState({
loading: false,
visible: false,
});
};
getMcm = () => {
const {packageName} = this.props;
this.setState({loading: true});
//send request to the invoker
axios.get(
window.location.origin + this.config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/managed-configs/package/" + packageName,
).then(res => {
if (res.status === 200) {
let mcmId = null;
if (res.data.hasOwnProperty("data")) {
mcmId = res.data.data.mcmId;
}
this.loadIframe(mcmId);
this.setState({loading: false});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load configurations.");
this.setState({loading: false, visible: false});
});
};
loadIframe = (mcmId) => {
const {packageName} = this.props;
let method = "post";
gapi.load('gapi.iframes', () => {
const parameters = {
token: this.config.androidEnterpriseToken,
packageName: packageName
};
if (mcmId != null) {
parameters.mcmId = mcmId;
parameters.canDelete = true;
method = "put";
}
const queryString = Object.keys(parameters).map(key => key + '=' + parameters[key]).join('&');
var options = {
'url': "https://play.google.com/managed/mcm?" + queryString,
'where': document.getElementById('manage-config-iframe-container'),
'attributes': {style: 'height:720px', scrolling: 'yes'}
};
var iframe = gapi.iframes.getContext().openChild(options);
iframe.register('onconfigupdated', (event) => {
this.updateConfig(method, event);
}, gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER);
iframe.register('onconfigdeleted', (event) => {
this.deleteConfig(event);
}, gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER);
});
};
updateConfig = (method, event) => {
const {packageName} = this.props;
this.setState({loading: true});
const data = {
mcmId: event.mcmId,
profileName: event.name,
packageName
};
//send request to the invoker
axios({
method,
url: window.location.origin + this.config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/managed-configs",
data
}).then(res => {
if (res.status === 200 || res.status === 201) {
notification["success"]({
message: 'Saved!',
description: 'Configuration Profile updated Successfully',
});
this.setState({
loading: false,
visible: false
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to update configurations.");
this.setState({loading: false});
});
};
deleteConfig = (event) => {
const {packageName} = this.props;
this.setState({loading: true});
//send request to the invoker
axios.delete(
window.location.origin + this.config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/managed-configs/mcm/" + event.mcmId
).then(res => {
if (res.status === 200 || res.status === 201) {
notification["success"]({
message: 'Saved!',
description: 'Configuration Profile removed Successfully',
});
this.setState({
loading: false,
visible: false
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to remove configurations.");
this.setState({loading: false});
});
};
render() {
return (
<div>
<Button
size="small"
type="primary"
icon="setting"
onClick={this.showModal}>
Manage
</Button>
<Modal
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
footer={null}>
<Spin spinning={this.state.loading}>
<div id="manage-config-iframe-container">
</div>
</Spin>
</Modal>
</div>
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to update configurations.',
);
this.setState({ loading: false });
});
};
deleteConfig = event => {
this.setState({ loading: true });
// send request to the invoker
axios
.delete(
window.location.origin +
this.config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/managed-configs/mcm/' +
event.mcmId,
)
.then(res => {
if (res.status === 200 || res.status === 201) {
notification.success({
message: 'Saved!',
description: 'Configuration Profile removed Successfully',
});
this.setState({
loading: false,
visible: false,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to remove configurations.',
);
}
this.setState({ loading: false });
});
};
render() {
return (
<div>
<Button
size="small"
type="primary"
icon="setting"
onClick={this.showModal}
>
Manage
</Button>
<Modal
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
footer={null}
>
<Spin spinning={this.state.loading}>
<div id="manage-config-iframe-container"></div>
</Spin>
</Modal>
</div>
);
}
}
export default withConfigContext(ManagedConfigurationsIframe);
export default withConfigContext(ManagedConfigurationsIframe);

@ -16,107 +16,107 @@
* under the License.
*/
import React from "react";
import {Modal, Icon, Table, Avatar} from 'antd';
import "../Cluster.css";
import {withConfigContext} from "../../../../../../context/ConfigContext";
import React from 'react';
import { Modal, Icon, Table, Avatar } from 'antd';
import '../Cluster.css';
import { withConfigContext } from '../../../../../../context/ConfigContext';
const columns = [
{
title: '',
dataIndex: 'iconUrl',
key: 'iconUrl',
render: (iconUrl) => (<Avatar shape="square" src={iconUrl}/>)
},
{
title: 'Name',
dataIndex: 'name',
key: 'name'
},
{
title: 'Page',
dataIndex: 'packageId',
key: 'packageId'
}
{
title: '',
dataIndex: 'iconUrl',
key: 'iconUrl',
// eslint-disable-next-line react/display-name
render: iconUrl => <Avatar shape="square" src={iconUrl} />,
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Page',
dataIndex: 'packageId',
key: 'packageId',
},
];
class AddAppsToClusterModal extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
loading: false,
selectedProducts: [],
homePageId: null
};
}
showModal = () => {
this.setState({
visible: true,
});
constructor(props) {
super(props);
this.state = {
visible: false,
loading: false,
selectedProducts: [],
homePageId: null,
};
}
handleOk = () => {
this.props.addSelectedProducts(this.state.selectedProducts);
this.handleCancel();
};
showModal = () => {
this.setState({
visible: true,
});
};
handleCancel = () => {
this.setState({
visible: false,
});
};
handleOk = () => {
this.props.addSelectedProducts(this.state.selectedProducts);
this.handleCancel();
};
rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
this.setState({
selectedProducts: selectedRows
})
},
};
handleCancel = () => {
this.setState({
visible: false,
});
};
rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
this.setState({
selectedProducts: selectedRows,
});
},
};
render() {
const {pagination, loading} = this.state;
return (
<div>
<div className="btn-add-new-wrapper">
<div className="btn-add-new">
<button className="btn"
onClick={this.showModal}>
<Icon style={{position: "relative"}} type="plus"/>
</button>
</div>
<div className="title">
Add app
</div>
</div>
<Modal
title="Select Apps"
width={640}
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}>
<Table
columns={columns}
rowKey={record => record.packageId}
dataSource={this.props.unselectedProducts}
scroll={{ x: 300 }}
pagination={{
...pagination,
size: "small",
// position: "top",
showTotal: (total, range) => `showing ${range[0]}-${range[1]} of ${total} pages`,
showQuickJumper: true
}}
loading={loading}
onChange={this.handleTableChange}
rowSelection={this.rowSelection}
/>
</Modal>
</div>
);
}
render() {
const { pagination, loading } = this.state;
return (
<div>
<div className="btn-add-new-wrapper">
<div className="btn-add-new">
<button className="btn" onClick={this.showModal}>
<Icon style={{ position: 'relative' }} type="plus" />
</button>
</div>
<div className="title">Add app</div>
</div>
<Modal
title="Select Apps"
width={640}
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
>
<Table
columns={columns}
rowKey={record => record.packageId}
dataSource={this.props.unselectedProducts}
scroll={{ x: 300 }}
pagination={{
...pagination,
size: 'small',
// position: "top",
showTotal: (total, range) =>
`showing ${range[0]}-${range[1]} of ${total} pages`,
showQuickJumper: true,
}}
loading={loading}
onChange={this.handleTableChange}
rowSelection={this.rowSelection}
/>
</Modal>
</div>
);
}
}
export default withConfigContext(AddAppsToClusterModal);
export default withConfigContext(AddAppsToClusterModal);

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

@ -16,103 +16,110 @@
* under the License.
*/
import React from "react";
import {Button, message, Modal, notification, Select, Spin} from "antd";
import axios from "axios";
import {withConfigContext} from "../../../../../context/ConfigContext";
import {handleApiError} from "../../../../../js/Utils";
import React from 'react';
import { Button, Modal, notification, Select, Spin } from 'antd';
import axios from 'axios';
import { withConfigContext } from '../../../../../context/ConfigContext';
import { handleApiError } from '../../../../../js/Utils';
const {Option} = Select;
const { Option } = Select;
class EditLinks extends React.Component {
constructor(props) {
super(props);
this.selectedLinks = [];
this.state = {
visible: false
};
}
showModal = () => {
this.setState({
visible: true,
loading: false
});
constructor(props) {
super(props);
this.selectedLinks = [];
this.state = {
visible: false,
};
}
showModal = () => {
this.setState({
visible: true,
loading: false,
});
};
handleCancel = e => {
this.setState({
visible: false,
});
};
updateLinks = () => {
const config = this.props.context;
this.setState({loading: true});
handleCancel = e => {
this.setState({
visible: false,
});
};
axios.put(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/page-link",
{
pageId: this.props.pageId,
links: this.selectedLinks
}
).then(res => {
if (res.status === 200) {
updateLinks = () => {
const config = this.props.context;
this.setState({ loading: true });
notification["success"]({
message: 'Saved!',
description: 'Links updated successfully!'
});
axios
.put(
window.location.origin +
config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/store-layout/page-link',
{
pageId: this.props.pageId,
links: this.selectedLinks,
},
)
.then(res => {
if (res.status === 200) {
notification.success({
message: 'Saved!',
description: 'Links updated successfully!',
});
this.props.updateLinks(this.selectedLinks);
this.props.updateLinks(this.selectedLinks);
this.setState({
loading: false,
visible: false
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to update the cluster.");
this.setState({loading: false});
});
};
this.setState({
loading: false,
visible: false,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to update the cluster.',
);
this.setState({ loading: false });
});
};
handleChange= (selectedLinks) =>{
this.selectedLinks = selectedLinks;
};
handleChange = selectedLinks => {
this.selectedLinks = selectedLinks;
};
render() {
return (
<div>
<Button onClick={this.showModal} type="link">[add / remove links]</Button>
<Modal
title="Add / Remove Links"
visible={this.state.visible}
onOk={this.updateLinks}
onCancel={this.handleCancel}
okText="Update">
<Spin spinning={this.state.loading}>
<Select
mode="multiple"
style={{width: '100%'}}
placeholder="Please select links"
defaultValue={this.props.selectedLinks}
onChange={this.handleChange}>
{
this.props.pages.map((page) => (
<Option disabled={page.id===this.props.pageId} key={page.id}>
{page.name[0]["text"]}
</Option>))
}
</Select>
</Spin>
</Modal>
</div>
);
}
render() {
return (
<div>
<Button onClick={this.showModal} type="link">
[add / remove links]
</Button>
<Modal
title="Add / Remove Links"
visible={this.state.visible}
onOk={this.updateLinks}
onCancel={this.handleCancel}
okText="Update"
>
<Spin spinning={this.state.loading}>
<Select
mode="multiple"
style={{ width: '100%' }}
placeholder="Please select links"
defaultValue={this.props.selectedLinks}
onChange={this.handleChange}
>
{this.props.pages.map(page => (
<Option disabled={page.id === this.props.pageId} key={page.id}>
{page.name[0].text}
</Option>
))}
</Select>
</Spin>
</Modal>
</div>
);
}
}
export default withConfigContext(EditLinks);
export default withConfigContext(EditLinks);

@ -16,247 +16,277 @@
* under the License.
*/
import React from "react";
import axios from "axios";
import {Tag, message, notification, Table, Typography, Divider, Icon, Popconfirm, Button} from "antd";
import React from 'react';
import axios from 'axios';
import {
Tag,
notification,
Table,
Typography,
Divider,
Icon,
Popconfirm,
Button,
} from 'antd';
import {withConfigContext} from "../../../../context/ConfigContext";
import "./Pages.css";
import {Link} from "react-router-dom";
import AddNewPage from "../AddNewPage/AddNewPage";
import {handleApiError} from "../../../../js/Utils";
const {Text, Title} = Typography;
let config = null;
import { withConfigContext } from '../../../../context/ConfigContext';
import './Pages.css';
import { Link } from 'react-router-dom';
import AddNewPage from '../AddNewPage/AddNewPage';
import { handleApiError } from '../../../../js/Utils';
const { Text, Title } = Typography;
class Pages extends React.Component {
constructor(props) {
super(props);
config = this.props.context;
// TimeAgo.addLocale(en);
this.state = {
data: [],
pagination: {},
loading: false,
selectedRows: [],
homePageId: null
};
}
rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
this.setState({
selectedRows: selectedRows
})
}
constructor(props) {
super(props);
this.state = {
data: [],
pagination: {},
loading: false,
selectedRows: [],
homePageId: null,
};
}
componentDidMount() {
this.setHomePage();
this.fetch();
}
//fetch data from api
fetch = (params = {}) => {
const config = this.props.context;
this.setState({loading: true});
// get current page
const currentPage = (params.hasOwnProperty("page")) ? params.page : 1;
rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
this.setState({
selectedRows: selectedRows,
});
},
};
const extraParams = {
offset: 10 * (currentPage - 1), //calculate the offset
limit: 10,
};
componentDidMount() {
this.setHomePage();
this.fetch();
}
//send request to the invoker
axios.get(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/page",
).then(res => {
if (res.status === 200) {
const pagination = {...this.state.pagination};
this.setState({
loading: false,
data: res.data.data.page,
pagination,
});
}
// fetch data from api
fetch = (params = {}) => {
const config = this.props.context;
this.setState({ loading: true });
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load pages.");
this.setState({loading: false});
});
};
setHomePage = () => {
const config = this.props.context;
//send request to the invoker
axios.get(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/home-page",
).then(res => {
if (res.status === 200) {
this.setState({
homePageId: res.data.data.homepageId
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to get home page.");
this.setState({loading: false});
});
};
updateHomePage = (pageId) => {
const config = this.props.context;
this.setState({
loading: true
});
//send request to the invoker
axios.put(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/home-page/" + pageId,
{}
).then(res => {
if (res.status === 200) {
notification["success"]({
message: "Done!",
description:
"Home page was updated successfully!",
});
// send request to the invoker
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/store-layout/page',
)
.then(res => {
if (res.status === 200) {
const pagination = { ...this.state.pagination };
this.setState({
loading: false,
data: res.data.data.page,
pagination,
});
}
})
.catch(error => {
handleApiError(error, 'Error occurred while trying to load pages.');
this.setState({ loading: false });
});
};
this.setState({
homePageId: res.data.data.homepageId,
loading: false
});
}
setHomePage = () => {
const config = this.props.context;
// send request to the invoker
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/store-layout/home-page',
)
.then(res => {
if (res.status === 200) {
this.setState({
homePageId: res.data.data.homepageId,
});
}
})
.catch(error => {
handleApiError(error, 'Error occurred while trying to get home page.');
this.setState({ loading: false });
});
};
}).catch((error) => {
handleApiError(error, "Error occurred while trying to update the home page.");
this.setState({loading: false});
});
};
updateHomePage = pageId => {
const config = this.props.context;
this.setState({
loading: true,
});
// send request to the invoker
axios
.put(
window.location.origin +
config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/store-layout/home-page/' +
pageId,
{},
)
.then(res => {
if (res.status === 200) {
notification.success({
message: 'Done!',
description: 'Home page was updated successfully!',
});
deletePage = (pageId) => {
const {data} = this.state;
const config = this.props.context;
this.setState({
loading: true
});
//send request to the invoker
axios.delete(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/page/" + pageId
).then(res => {
if (res.status === 200) {
notification["success"]({
message: "Done!",
description:
"Home page was updated successfully!",
});
this.setState({
homePageId: res.data.data.homepageId,
loading: false,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to update the home page.',
);
this.setState({ loading: false });
});
};
for( let i = 0; i < data.length; i++){
if ( data[i].id === pageId) {
data.splice(i, 1);
}
}
deletePage = pageId => {
const { data } = this.state;
const config = this.props.context;
this.setState({
loading: true,
});
// send request to the invoker
axios
.delete(
window.location.origin +
config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/store-layout/page/' +
pageId,
)
.then(res => {
if (res.status === 200) {
notification.success({
message: 'Done!',
description: 'Home page was updated successfully!',
});
this.setState({
loading: false,
data: data
});
for (let i = 0; i < data.length; i++) {
if (data[i].id === pageId) {
data.splice(i, 1);
}
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to delete the page.");
this.setState({loading: false});
});
};
handleTableChange = (pagination, filters, sorter) => {
const pager = {...this.state.pagination};
pager.current = pagination.current;
this.setState({
pagination: pager,
});
};
columns = [
{
title: 'Page',
dataIndex: 'name',
key: 'name',
width: 300,
render: (name, page) => {
const pageName = name[0].text;
return (<div>
<Link to={`/publisher/manage/android-enterprise/pages/${pageName}/${page.id}`}> {pageName + " "}</Link>
{(page.id === this.state.homePageId) && (<Tag color="#badc58">Home Page</Tag>)}
</div>)
this.setState({
loading: false,
data: data,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to delete the page.',
);
this.setState({ loading: false });
});
};
}
},
{
title: 'Actions',
key: 'actions',
render: (name, page) => (
<div>
<span className="action">
<Button disabled={page.id === this.state.homePageId}
className="btn-warning"
icon="home"
type="link"
onClick={() => {
this.updateHomePage(page.id);
}}>
set as homepage
</Button>
</span>
<Divider type="vertical"/>
<Popconfirm
title="Are you sure"
okText="Yes"
cancelText="No"
onConfirm={() => {
this.deletePage(page.id);
}}>
<span className="action">
<Text type="danger"><Icon type="delete"/> delete</Text>
</span>
</Popconfirm>
</div>
),
},
];
handleTableChange = (pagination, filters, sorter) => {
const pager = { ...this.state.pagination };
pager.current = pagination.current;
this.setState({
pagination: pager,
});
};
render() {
const {data, pagination, loading, selectedRows} = this.state;
columns = [
{
title: 'Page',
dataIndex: 'name',
key: 'name',
width: 300,
render: (name, page) => {
const pageName = name[0].text;
return (
<div className="layout-pages">
<Title level={4}>Pages</Title>
<AddNewPage/>
<div style={{backgroundColor: "#ffffff", borderRadius: 5}}>
<Table
columns={this.columns}
rowKey={record => record.id}
dataSource={data}
pagination={{
...pagination,
size: "small",
// position: "top",
showTotal: (total, range) => `showing ${range[0]}-${range[1]} of ${total} pages`,
showQuickJumper: true
}}
loading={loading}
onChange={this.handleTableChange}
// rowSelection={this.rowSelection}
scroll={{x: 1000}}
/>
</div>
</div>
<div>
<Link
to={`/publisher/manage/android-enterprise/pages/${pageName}/${page.id}`}
>
{' '}
{pageName + ' '}
</Link>
{page.id === this.state.homePageId && (
<Tag color="#badc58">Home Page</Tag>
)}
</div>
);
}
},
},
{
title: 'Actions',
key: 'actions',
render: (name, page) => (
<div>
<span className="action">
<Button
disabled={page.id === this.state.homePageId}
className="btn-warning"
icon="home"
type="link"
onClick={() => {
this.updateHomePage(page.id);
}}
>
set as homepage
</Button>
</span>
<Divider type="vertical" />
<Popconfirm
title="Are you sure"
okText="Yes"
cancelText="No"
onConfirm={() => {
this.deletePage(page.id);
}}
>
<span className="action">
<Text type="danger">
<Icon type="delete" /> delete
</Text>
</span>
</Popconfirm>
</div>
),
},
];
render() {
const { data, pagination, loading } = this.state;
return (
<div className="layout-pages">
<Title level={4}>Pages</Title>
<AddNewPage />
<div style={{ backgroundColor: '#ffffff', borderRadius: 5 }}>
<Table
columns={this.columns}
rowKey={record => record.id}
dataSource={data}
pagination={{
...pagination,
size: 'small',
// position: "top",
showTotal: (total, range) =>
`showing ${range[0]}-${range[1]} of ${total} pages`,
showQuickJumper: true,
}}
loading={loading}
onChange={this.handleTableChange}
// rowSelection={this.rowSelection}
scroll={{ x: 1000 }}
/>
</div>
</div>
);
}
}
export default withConfigContext(Pages);
export default withConfigContext(Pages);

@ -16,64 +16,66 @@
* under the License.
*/
import React from "react";
import {Button, notification} from "antd";
import axios from "axios";
import {withConfigContext} from "../../../context/ConfigContext";
import {handleApiError} from "../../../js/Utils";
import React from 'react';
import { Button, notification } from 'antd';
import axios from 'axios';
import { withConfigContext } from '../../../context/ConfigContext';
import { handleApiError } from '../../../js/Utils';
class SyncAndroidApps extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
};
}
constructor(props) {
super(props);
this.state = {
loading: false
}
}
syncApps = () => {
const config = this.props.context;
this.setState({
loading: true,
});
syncApps = () => {
const config = this.props.context;
this.setState({
loading: true
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/products/sync',
)
.then(res => {
notification.success({
message: 'Done!',
description: 'Apps synced successfully!',
});
axios.get(
window.location.origin + config.serverConfig.invoker.uri + "/device-mgt/android/v1.0/enterprise/products/sync",
).then(res => {
notification["success"]({
message: "Done!",
description:
"Apps synced successfully!",
});
this.setState({
loading: false
});
}).catch((error) => {
handleApiError(error, "Error occurred while syncing the apps.");
this.setState({
loading: false
})
this.setState({
loading: false,
});
};
})
.catch(error => {
handleApiError(error, 'Error occurred while syncing the apps.');
this.setState({
loading: false,
});
});
};
render() {
const {loading} = this.state;
return (
<div style={{display: "inline-block", padding: 4}}>
<Button
onClick={this.syncApps}
loading={loading}
style={{marginTop: 16}}
type="primary"
icon="sync"
>
Sync{loading && "ing..."}
</Button>
</div>
)
}
render() {
const { loading } = this.state;
return (
<div style={{ display: 'inline-block', padding: 4 }}>
<Button
onClick={this.syncApps}
loading={loading}
style={{ marginTop: 16 }}
type="primary"
icon="sync"
>
Sync{loading && 'ing...'}
</Button>
</div>
);
}
}
export default withConfigContext(SyncAndroidApps);
export default withConfigContext(SyncAndroidApps);

@ -16,452 +16,507 @@
* under the License.
*/
import React from "react";
import React from 'react';
import {
Card,
Tag,
message,
Icon,
Input,
notification,
Divider,
Button,
Spin,
Tooltip,
Popconfirm,
Modal,
Row,
Col,
Typography, Alert
} from "antd";
import axios from "axios";
import {TweenOneGroup} from 'rc-tween-one';
import pSBC from "shade-blend-color";
import {withConfigContext} from "../../../context/ConfigContext";
import {handleApiError} from "../../../js/Utils";
const {Title} = Typography;
Card,
Tag,
message,
Icon,
Input,
notification,
Divider,
Button,
Spin,
Tooltip,
Popconfirm,
Modal,
Row,
Col,
Typography,
Alert,
} from 'antd';
import axios from 'axios';
import { TweenOneGroup } from 'rc-tween-one';
import pSBC from 'shade-blend-color';
import { withConfigContext } from '../../../context/ConfigContext';
import { handleApiError } from '../../../js/Utils';
const { Title } = Typography;
class ManageCategories extends React.Component {
state = {
loading: false,
searchText: '',
categories: [],
tempElements: [],
inputVisible: false,
inputValue: '',
isAddNewVisible: false,
isEditModalVisible: false,
currentlyEditingId: null,
editingValue: null,
forbiddenErrors: {
categories: false
state = {
loading: false,
searchText: '',
categories: [],
tempElements: [],
inputVisible: false,
inputValue: '',
isAddNewVisible: false,
isEditModalVisible: false,
currentlyEditingId: null,
editingValue: null,
forbiddenErrors: {
categories: false,
},
};
componentDidMount() {
const config = this.props.context;
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/applications/categories',
)
.then(res => {
if (res.status === 200) {
let categories = JSON.parse(res.data.data);
this.setState({
categories: categories,
loading: false,
});
}
};
componentDidMount() {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications/categories",
).then(res => {
if (res.status === 200) {
let categories = JSON.parse(res.data.data);
this.setState({
categories: categories,
loading: false
});
}
}).catch((error) => {
handleApiError(error, "Error occured while trying to load categories", true);
if (error.hasOwnProperty("response") && error.response.status === 403) {
const {forbiddenErrors} = this.state;
forbiddenErrors.categories = true;
this.setState({
forbiddenErrors,
loading: false
})
} else {
this.setState({
loading: false
});
}
});
}
handleCloseButton = () => {
this.setState({
tempElements: [],
isAddNewVisible: false
});
};
deleteCategory = (id) => {
const config = this.props.context;
this.setState({
loading: true
});
axios.delete(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/admin/applications/categories/" + id,
).then(res => {
if (res.status === 200) {
notification["success"]({
message: "Done!",
description:
"Category Removed Successfully!",
});
const {categories} = this.state;
const remainingElements = categories.filter(function (value) {
return value.categoryName !== id;
});
this.setState({
loading: false,
categories: remainingElements
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load categories.");
this.setState({
loading: false
});
});
};
renderElement = (category) => {
const config = this.props.context;
const categoryName = category.categoryName;
const tagElem = (
<Tag
color={pSBC(0.30, config.theme.primaryColor)}
style={{marginTop: 8}}>
{categoryName}
<Divider type="vertical"/>
<Tooltip title="edit">
<Icon onClick={() => {
this.openEditModal(categoryName)
}} type="edit"/>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="delete">
<Popconfirm
title="Are you sure delete this category?"
onConfirm={() => {
if (category.isCategoryDeletable) {
this.deleteCategory(categoryName);
} else {
notification["error"]({
message: 'Cannot delete "' + categoryName + '"',
description:
"This category is currently used. Please unassign the category from apps.",
});
}
}}
okText="Yes"
cancelText="No">
<Icon type="delete"/>
</Popconfirm>
</Tooltip>
</Tag>
);
return (
<span key={category.categoryName} style={{display: 'inline-block'}}>
{tagElem}
</span>
);
};
renderTempElement = (category) => {
const config = this.props.context;
const tagElem = (
<Tag
style={{marginTop: 8}}
closable
onClose={e => {
e.preventDefault();
const {tempElements} = this.state;
const remainingElements = tempElements.filter(function (value) {
return value.categoryName !== category.categoryName;
});
this.setState({
tempElements: remainingElements
});
}}
>
{category.categoryName}
</Tag>
);
return (
<span key={category.categoryName} style={{display: 'inline-block'}}>
{tagElem}
</span>
})
.catch(error => {
handleApiError(
error,
'Error occured while trying to load categories',
true,
);
};
showInput = () => {
this.setState({inputVisible: true}, () => this.input.focus());
};
handleInputChange = e => {
this.setState({inputValue: e.target.value});
};
handleInputConfirm = () => {
const {inputValue, categories} = this.state;
let {tempElements} = this.state;
if (inputValue) {
if ((categories.findIndex(i => i.categoryName === inputValue) === -1) && (tempElements.findIndex(i => i.categoryName === inputValue) === -1)) {
tempElements = [...tempElements, {categoryName: inputValue, isCategoryDeletable: true}];
} else {
message.warning('Category already exists');
}
if (error.hasOwnProperty('response') && error.response.status === 403) {
const { forbiddenErrors } = this.state;
forbiddenErrors.categories = true;
this.setState({
forbiddenErrors,
loading: false,
});
} else {
this.setState({
loading: false,
});
}
this.setState({
tempElements,
inputVisible: false,
inputValue: '',
});
};
handleSave = () => {
const config = this.props.context;
const {tempElements, categories} = this.state;
});
}
handleCloseButton = () => {
this.setState({
tempElements: [],
isAddNewVisible: false,
});
};
deleteCategory = id => {
const config = this.props.context;
this.setState({
loading: true,
});
axios
.delete(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/admin/applications/categories/' +
id,
)
.then(res => {
if (res.status === 200) {
notification.success({
message: 'Done!',
description: 'Category Removed Successfully!',
});
const { categories } = this.state;
const remainingElements = categories.filter(function(value) {
return value.categoryName !== id;
});
this.setState({
loading: false,
categories: remainingElements,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load categories.',
);
this.setState({
loading: true
loading: false,
});
const data = tempElements.map(category => category.categoryName);
axios.post(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/admin/applications/categories",
data,
).then(res => {
if (res.status === 200) {
notification["success"]({
message: "Done!",
description:
"New Categories were added successfully",
});
this.setState({
categories: [...categories, ...tempElements],
tempElements: [],
inputVisible: false,
inputValue: '',
loading: false,
isAddNewVisible: false
});
};
renderElement = category => {
const config = this.props.context;
const categoryName = category.categoryName;
const tagElem = (
<Tag
color={pSBC(0.3, config.theme.primaryColor)}
style={{ marginTop: 8 }}
>
{categoryName}
<Divider type="vertical" />
<Tooltip title="edit">
<Icon
onClick={() => {
this.openEditModal(categoryName);
}}
type="edit"
/>
</Tooltip>
<Divider type="vertical" />
<Tooltip title="delete">
<Popconfirm
title="Are you sure delete this category?"
onConfirm={() => {
if (category.isCategoryDeletable) {
this.deleteCategory(categoryName);
} else {
notification.error({
message: 'Cannot delete "' + categoryName + '"',
description:
'This category is currently used. Please unassign the category from apps.',
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to add categories.");
this.setState({
loading: false
});
});
};
saveInputRef = input => (this.input = input);
closeEditModal = e => {
this.setState({
isEditModalVisible: false,
currentlyEditingId: null
});
};
openEditModal = (id) => {
this.setState({
isEditModalVisible: true,
currentlyEditingId: id,
editingValue: id
})
};
editItem = () => {
const config = this.props.context;
const {editingValue, currentlyEditingId, categories} = this.state;
}
}}
okText="Yes"
cancelText="No"
>
<Icon type="delete" />
</Popconfirm>
</Tooltip>
</Tag>
);
return (
<span key={category.categoryName} style={{ display: 'inline-block' }}>
{tagElem}
</span>
);
};
renderTempElement = category => {
const tagElem = (
<Tag
style={{ marginTop: 8 }}
closable
onClose={e => {
e.preventDefault();
const { tempElements } = this.state;
const remainingElements = tempElements.filter(function(value) {
return value.categoryName !== category.categoryName;
});
this.setState({
tempElements: remainingElements,
});
}}
>
{category.categoryName}
</Tag>
);
return (
<span key={category.categoryName} style={{ display: 'inline-block' }}>
{tagElem}
</span>
);
};
showInput = () => {
this.setState({ inputVisible: true }, () => this.input.focus());
};
handleInputChange = e => {
this.setState({ inputValue: e.target.value });
};
handleInputConfirm = () => {
const { inputValue, categories } = this.state;
let { tempElements } = this.state;
if (inputValue) {
if (
categories.findIndex(i => i.categoryName === inputValue) === -1 &&
tempElements.findIndex(i => i.categoryName === inputValue) === -1
) {
tempElements = [
...tempElements,
{ categoryName: inputValue, isCategoryDeletable: true },
];
} else {
message.warning('Category already exists');
}
}
this.setState({
tempElements,
inputVisible: false,
inputValue: '',
});
};
handleSave = () => {
const config = this.props.context;
const { tempElements, categories } = this.state;
this.setState({
loading: true,
});
const data = tempElements.map(category => category.categoryName);
axios
.post(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/admin/applications/categories',
data,
)
.then(res => {
if (res.status === 200) {
notification.success({
message: 'Done!',
description: 'New Categories were added successfully',
});
this.setState({
categories: [...categories, ...tempElements],
tempElements: [],
inputVisible: false,
inputValue: '',
loading: false,
isAddNewVisible: false,
});
}
})
.catch(error => {
handleApiError(error, 'Error occurred while trying to add categories.');
this.setState({
loading: true,
isEditModalVisible: false,
});
axios.put(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/admin/applications/categories/rename?from=" + currentlyEditingId + "&to=" + editingValue,
{},
).then(res => {
if (res.status === 200) {
notification["success"]({
message: "Done!",
description:
"Category was edited successfully",
});
categories[categories.findIndex(i => i.categoryName === currentlyEditingId)].categoryName = editingValue;
this.setState({
categories: categories,
loading: false,
editingValue: null
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to delete the category.");
this.setState({
loading: false,
editingValue: null
});
loading: false,
});
};
handleEditInputChange = (e) => {
});
};
saveInputRef = input => (this.input = input);
closeEditModal = e => {
this.setState({
isEditModalVisible: false,
currentlyEditingId: null,
});
};
openEditModal = id => {
this.setState({
isEditModalVisible: true,
currentlyEditingId: id,
editingValue: id,
});
};
editItem = () => {
const config = this.props.context;
const { editingValue, currentlyEditingId, categories } = this.state;
this.setState({
loading: true,
isEditModalVisible: false,
});
axios
.put(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/admin/applications/categories/rename?from=' +
currentlyEditingId +
'&to=' +
editingValue,
{},
)
.then(res => {
if (res.status === 200) {
notification.success({
message: 'Done!',
description: 'Category was edited successfully',
});
categories[
categories.findIndex(i => i.categoryName === currentlyEditingId)
].categoryName = editingValue;
this.setState({
categories: categories,
loading: false,
editingValue: null,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to delete the category.',
);
this.setState({
editingValue: e.target.value
loading: false,
editingValue: null,
});
};
render() {
const {categories, inputVisible, inputValue, tempElements, isAddNewVisible, forbiddenErrors} = this.state;
const categoriesElements = categories.map(this.renderElement);
const temporaryElements = tempElements.map(this.renderTempElement);
return (
<div style={{marginBottom: 16}}>
{(forbiddenErrors.categories) && (
<Alert
message="You don't have permission to view categories."
type="warning"
banner
closable/>
});
};
handleEditInputChange = e => {
this.setState({
editingValue: e.target.value,
});
};
render() {
const {
categories,
inputVisible,
inputValue,
tempElements,
isAddNewVisible,
forbiddenErrors,
} = this.state;
const categoriesElements = categories.map(this.renderElement);
const temporaryElements = tempElements.map(this.renderTempElement);
return (
<div style={{ marginBottom: 16 }}>
{forbiddenErrors.categories && (
<Alert
message="You don't have permission to view categories."
type="warning"
banner
closable
/>
)}
<Card>
<Spin tip="Working on it..." spinning={this.state.loading}>
<Row>
<Col span={16}>
<Title level={4}>Categories</Title>
</Col>
<Col span={8}>
{!isAddNewVisible && (
<div style={{ float: 'right' }}>
<Button
icon="plus"
// type="primary"
size="small"
onClick={() => {
this.setState(
{
isAddNewVisible: true,
inputVisible: true,
},
() => this.input.focus(),
);
}}
htmlType="button"
>
Add
</Button>
</div>
)}
<Card>
<Spin tip="Working on it..." spinning={this.state.loading}>
<Row>
<Col span={16}>
<Title level={4}>Categories</Title>
</Col>
<Col span={8}>
{!isAddNewVisible &&
<div style={{float: "right"}}>
<Button
icon="plus"
// type="primary"
size="small"
onClick={() => {
this.setState({
isAddNewVisible: true,
inputVisible: true
}, () => this.input.focus())
}} htmlType="button">Add
</Button>
</div>
}
</Col>
</Row>
{isAddNewVisible &&
<div>
<Divider/>
<div style={{marginBottom: 16}}>
<TweenOneGroup
enter={{
scale: 0.8,
opacity: 0,
type: 'from',
duration: 100,
onComplete: e => {
e.target.style = '';
},
}}
leave={{opacity: 0, width: 0, scale: 0, duration: 200}}
appear={false}
>
{temporaryElements}
{inputVisible && (
<Input
ref={this.saveInputRef}
type="text"
size="small"
style={{width: 120}}
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 Category
</Tag>
)}
</TweenOneGroup>
</div>
<div>
{tempElements.length > 0 && (
<span>
<Button
onClick={this.handleSave}
htmlType="button" type="primary"
size="small"
>
Save
</Button>
<Divider type="vertical"/>
</span>
)}
<Button
onClick={this.handleCloseButton}
size="small">
Cancel
</Button>
</div>
</div>
}
<Divider dashed="true"/>
<div style={{marginTop: 8}}>
<TweenOneGroup
enter={{
scale: 0.8,
opacity: 0,
type: 'from',
duration: 100,
onComplete: e => {
e.target.style = '';
},
}}
leave={{opacity: 0, width: 0, scale: 0, duration: 200}}
appear={false}
>
{categoriesElements}
</TweenOneGroup>
</div>
</Spin>
</Card>
<Modal
title="Edit"
visible={this.state.isEditModalVisible}
onCancel={this.closeEditModal}
onOk={this.editItem}
>
<Input value={this.state.editingValue} ref={(input) => this.editingInput = input}
onChange={this.handleEditInputChange}/>
</Modal>
</Col>
</Row>
{isAddNewVisible && (
<div>
<Divider />
<div style={{ marginBottom: 16 }}>
<TweenOneGroup
enter={{
scale: 0.8,
opacity: 0,
type: 'from',
duration: 100,
onComplete: e => {
e.target.style = '';
},
}}
leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}
appear={false}
>
{temporaryElements}
{inputVisible && (
<Input
ref={this.saveInputRef}
type="text"
size="small"
style={{ width: 120 }}
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 Category
</Tag>
)}
</TweenOneGroup>
</div>
<div>
{tempElements.length > 0 && (
<span>
<Button
onClick={this.handleSave}
htmlType="button"
type="primary"
size="small"
>
Save
</Button>
<Divider type="vertical" />
</span>
)}
<Button onClick={this.handleCloseButton} size="small">
Cancel
</Button>
</div>
</div>
)}
<Divider dashed="true" />
<div style={{ marginTop: 8 }}>
<TweenOneGroup
enter={{
scale: 0.8,
opacity: 0,
type: 'from',
duration: 100,
onComplete: e => {
e.target.style = '';
},
}}
leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}
appear={false}
>
{categoriesElements}
</TweenOneGroup>
</div>
);
}
</Spin>
</Card>
<Modal
title="Edit"
visible={this.state.isEditModalVisible}
onCancel={this.closeEditModal}
onOk={this.editItem}
>
<Input
value={this.state.editingValue}
ref={input => (this.editingInput = input)}
onChange={this.handleEditInputChange}
/>
</Modal>
</div>
);
}
}
export default withConfigContext(ManageCategories);

@ -16,450 +16,499 @@
* under the License.
*/
import React from "react";
import React from 'react';
import {
Card,
Tag,
message,
Icon,
Input,
notification,
Divider,
Button,
Spin,
Tooltip,
Popconfirm,
Modal,
Row, Col,
Typography, Alert
} from "antd";
import axios from "axios";
import {TweenOneGroup} from 'rc-tween-one';
import {withConfigContext} from "../../../context/ConfigContext";
import {handleApiError} from "../../../js/Utils";
const {Title} = Typography;
Card,
Tag,
message,
Icon,
Input,
notification,
Divider,
Button,
Spin,
Tooltip,
Popconfirm,
Modal,
Row,
Col,
Typography,
Alert,
} from 'antd';
import axios from 'axios';
import { TweenOneGroup } from 'rc-tween-one';
import { withConfigContext } from '../../../context/ConfigContext';
import { handleApiError } from '../../../js/Utils';
const { Title } = Typography;
class ManageTags extends React.Component {
state = {
loading: false,
searchText: '',
tags: [],
tempElements: [],
inputVisible: false,
inputValue: '',
isAddNewVisible: false,
isEditModalVisible: false,
currentlyEditingId: null,
editingValue: null,
forbiddenErrors: {
tags: false
state = {
loading: false,
searchText: '',
tags: [],
tempElements: [],
inputVisible: false,
inputValue: '',
isAddNewVisible: false,
isEditModalVisible: false,
currentlyEditingId: null,
editingValue: null,
forbiddenErrors: {
tags: false,
},
};
componentDidMount() {
const config = this.props.context;
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/applications/tags',
)
.then(res => {
if (res.status === 200) {
let tags = JSON.parse(res.data.data);
this.setState({
tags: tags,
loading: false,
});
}
};
componentDidMount() {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications/tags",
).then(res => {
if (res.status === 200) {
let tags = JSON.parse(res.data.data);
this.setState({
tags: tags,
loading: false
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load tags.", true);
if (error.hasOwnProperty("response") && error.response.status === 403) {
const {forbiddenErrors} = this.state;
forbiddenErrors.tags = true;
this.setState({
forbiddenErrors,
loading: false
})
} else {
this.setState({
loading: false
});
}
});
}
handleCloseButton = () => {
this.setState({
tempElements: [],
isAddNewVisible: false
});
};
deleteTag = (id) => {
const config = this.props.context;
this.setState({
loading: true
});
axios.delete(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/admin/applications/tags/" + id
).then(res => {
if (res.status === 200) {
notification["success"]({
message: "Done!",
description:
"Tag Removed Successfully!",
});
const {tags} = this.state;
const remainingElements = tags.filter(function (value) {
return value.tagName !== id;
});
this.setState({
loading: false,
tags: remainingElements
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to delete the tag.");
this.setState({
loading: false
});
});
};
renderElement = (tag) => {
const tagName = tag.tagName;
const tagElem = (
<Tag
color="#34495e"
style={{marginTop: 8}}
>
{tagName}
<Divider type="vertical"/>
<Tooltip title="edit">
<Icon onClick={() => {
this.openEditModal(tagName)
}} type="edit"/>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="delete">
<Popconfirm
title="Are you sure delete this tag?"
onConfirm={() => {
if (tag.isTagDeletable) {
this.deleteTag(tagName);
} else {
notification["error"]({
message: 'Cannot delete "' + tagName + '"',
description:
"This tag is currently used. Please unassign the tag from apps.",
});
}
}}
okText="Yes"
cancelText="No"
>
<Icon type="delete"/>
</Popconfirm>
</Tooltip>
</Tag>
);
return (
<span key={tag.tagName} style={{display: 'inline-block'}}>
{tagElem}
</span>
);
};
renderTempElement = (tag) => {
const {tempElements} = this.state;
const tagElem = (
<Tag
style={{marginTop: 8}}
closable
onClose={e => {
e.preventDefault();
const remainingElements = tempElements.filter(function (value) {
return value.tagName !== tag.tagName;
});
this.setState({
tempElements: remainingElements
});
}}
>
{tag.tagName}
</Tag>
);
return (
<span key={tag.tagName} style={{display: 'inline-block'}}>
{tagElem}
</span>
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load tags.',
true,
);
};
showInput = () => {
this.setState({inputVisible: true}, () => this.input.focus());
};
handleInputChange = e => {
this.setState({inputValue: e.target.value});
};
handleInputConfirm = () => {
const {inputValue, tags} = this.state;
let {tempElements} = this.state;
if (inputValue) {
if ((tags.findIndex(i => i.tagName === inputValue) === -1) && (tempElements.findIndex(i => i.tagName === inputValue) === -1)) {
tempElements = [...tempElements, {tagName: inputValue, isTagDeletable: true}];
} else {
message.warning('Tag already exists');
}
if (error.hasOwnProperty('response') && error.response.status === 403) {
const { forbiddenErrors } = this.state;
forbiddenErrors.tags = true;
this.setState({
forbiddenErrors,
loading: false,
});
} else {
this.setState({
loading: false,
});
}
this.setState({
tempElements,
inputVisible: false,
inputValue: '',
});
};
handleSave = () => {
const config = this.props.context;
const {tempElements, tags} = this.state;
});
}
handleCloseButton = () => {
this.setState({
tempElements: [],
isAddNewVisible: false,
});
};
deleteTag = id => {
const config = this.props.context;
this.setState({
loading: true,
});
axios
.delete(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/admin/applications/tags/' +
id,
)
.then(res => {
if (res.status === 200) {
notification.success({
message: 'Done!',
description: 'Tag Removed Successfully!',
});
const { tags } = this.state;
const remainingElements = tags.filter(function(value) {
return value.tagName !== id;
});
this.setState({
loading: false,
tags: remainingElements,
});
}
})
.catch(error => {
handleApiError(error, 'Error occurred while trying to delete the tag.');
this.setState({
loading: true
loading: false,
});
const data = tempElements.map(tag => tag.tagName);
axios.post(window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications/tags",
data,
).then(res => {
if (res.status === 200) {
notification["success"]({
message: "Done!",
description:
"New tags were added successfully",
});
this.setState({
tags: [...tags, ...tempElements],
tempElements: [],
inputVisible: false,
inputValue: '',
loading: false,
isAddNewVisible: false
});
};
renderElement = tag => {
const tagName = tag.tagName;
const tagElem = (
<Tag color="#34495e" style={{ marginTop: 8 }}>
{tagName}
<Divider type="vertical" />
<Tooltip title="edit">
<Icon
onClick={() => {
this.openEditModal(tagName);
}}
type="edit"
/>
</Tooltip>
<Divider type="vertical" />
<Tooltip title="delete">
<Popconfirm
title="Are you sure delete this tag?"
onConfirm={() => {
if (tag.isTagDeletable) {
this.deleteTag(tagName);
} else {
notification.error({
message: 'Cannot delete "' + tagName + '"',
description:
'This tag is currently used. Please unassign the tag from apps.',
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to delete tag.");
this.setState({
loading: false
});
});
};
saveInputRef = input => (this.input = input);
closeEditModal = e => {
this.setState({
isEditModalVisible: false,
currentlyEditingId: null
});
};
openEditModal = (id) => {
this.setState({
isEditModalVisible: true,
currentlyEditingId: id,
editingValue: id
})
};
editItem = () => {
const config = this.props.context;
const {editingValue, currentlyEditingId, tags} = this.state;
}
}}
okText="Yes"
cancelText="No"
>
<Icon type="delete" />
</Popconfirm>
</Tooltip>
</Tag>
);
return (
<span key={tag.tagName} style={{ display: 'inline-block' }}>
{tagElem}
</span>
);
};
renderTempElement = tag => {
const { tempElements } = this.state;
const tagElem = (
<Tag
style={{ marginTop: 8 }}
closable
onClose={e => {
e.preventDefault();
const remainingElements = tempElements.filter(function(value) {
return value.tagName !== tag.tagName;
});
this.setState({
tempElements: remainingElements,
});
}}
>
{tag.tagName}
</Tag>
);
return (
<span key={tag.tagName} style={{ display: 'inline-block' }}>
{tagElem}
</span>
);
};
showInput = () => {
this.setState({ inputVisible: true }, () => this.input.focus());
};
handleInputChange = e => {
this.setState({ inputValue: e.target.value });
};
handleInputConfirm = () => {
const { inputValue, tags } = this.state;
let { tempElements } = this.state;
if (inputValue) {
if (
tags.findIndex(i => i.tagName === inputValue) === -1 &&
tempElements.findIndex(i => i.tagName === inputValue) === -1
) {
tempElements = [
...tempElements,
{ tagName: inputValue, isTagDeletable: true },
];
} else {
message.warning('Tag already exists');
}
}
this.setState({
tempElements,
inputVisible: false,
inputValue: '',
});
};
handleSave = () => {
const config = this.props.context;
const { tempElements, tags } = this.state;
this.setState({
loading: true,
});
const data = tempElements.map(tag => tag.tagName);
axios
.post(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/applications/tags',
data,
)
.then(res => {
if (res.status === 200) {
notification.success({
message: 'Done!',
description: 'New tags were added successfully',
});
this.setState({
tags: [...tags, ...tempElements],
tempElements: [],
inputVisible: false,
inputValue: '',
loading: false,
isAddNewVisible: false,
});
}
})
.catch(error => {
handleApiError(error, 'Error occurred while trying to delete tag.');
this.setState({
loading: true,
isEditModalVisible: false,
});
axios.put(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications/tags/rename?from=" + currentlyEditingId + "&to=" + editingValue,
{},
).then(res => {
if (res.status === 200) {
notification["success"]({
message: "Done!",
description:
"Tag was edited successfully",
});
tags[tags.findIndex(i => i.tagName === currentlyEditingId)].tagName = editingValue;
this.setState({
tags: tags,
loading: false,
editingValue: null
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to edit tag.");
this.setState({
loading: false,
editingValue: null
});
loading: false,
});
};
handleEditInputChange = (e) => {
});
};
saveInputRef = input => (this.input = input);
closeEditModal = e => {
this.setState({
isEditModalVisible: false,
currentlyEditingId: null,
});
};
openEditModal = id => {
this.setState({
isEditModalVisible: true,
currentlyEditingId: id,
editingValue: id,
});
};
editItem = () => {
const config = this.props.context;
const { editingValue, currentlyEditingId, tags } = this.state;
this.setState({
loading: true,
isEditModalVisible: false,
});
axios
.put(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/applications/tags/rename?from=' +
currentlyEditingId +
'&to=' +
editingValue,
{},
)
.then(res => {
if (res.status === 200) {
notification.success({
message: 'Done!',
description: 'Tag was edited successfully',
});
tags[
tags.findIndex(i => i.tagName === currentlyEditingId)
].tagName = editingValue;
this.setState({
tags: tags,
loading: false,
editingValue: null,
});
}
})
.catch(error => {
handleApiError(error, 'Error occurred while trying to edit tag.');
this.setState({
editingValue: e.target.value
loading: false,
editingValue: null,
});
};
render() {
const {tags, inputVisible, inputValue, tempElements, isAddNewVisible, forbiddenErrors} = this.state;
const tagsElements = tags.map(this.renderElement);
const temporaryElements = tempElements.map(this.renderTempElement);
return (
<div style={{marginBottom: 16}}>
{(forbiddenErrors.tags) && (
<Alert
message="You don't have permission to view tags."
type="warning"
banner
closable/>
});
};
handleEditInputChange = e => {
this.setState({
editingValue: e.target.value,
});
};
render() {
const {
tags,
inputVisible,
inputValue,
tempElements,
isAddNewVisible,
forbiddenErrors,
} = this.state;
const tagsElements = tags.map(this.renderElement);
const temporaryElements = tempElements.map(this.renderTempElement);
return (
<div style={{ marginBottom: 16 }}>
{forbiddenErrors.tags && (
<Alert
message="You don't have permission to view tags."
type="warning"
banner
closable
/>
)}
<Card>
<Spin tip="Working on it..." spinning={this.state.loading}>
<Row>
<Col span={16}>
<Title level={4}>Tags</Title>
</Col>
<Col span={8}>
{!isAddNewVisible && (
<div style={{ float: 'right' }}>
<Button
icon="plus"
// type="primary"
size="small"
onClick={() => {
this.setState(
{
isAddNewVisible: true,
inputVisible: true,
},
() => this.input.focus(),
);
}}
htmlType="button"
>
Add
</Button>
</div>
)}
<Card>
<Spin tip="Working on it..." spinning={this.state.loading}>
<Row>
<Col span={16}>
<Title level={4}>Tags</Title>
</Col>
<Col span={8}>
{!isAddNewVisible &&
<div style={{float: "right"}}>
<Button
icon="plus"
// type="primary"
size="small"
onClick={() => {
this.setState({
isAddNewVisible: true,
inputVisible: true
}, () => this.input.focus())
}} htmlType="button">Add
</Button>
</div>
}
</Col>
</Row>
{isAddNewVisible &&
<div>
<Divider/>
<div style={{marginBottom: 16}}>
<TweenOneGroup
enter={{
scale: 0.8,
opacity: 0,
type: 'from',
duration: 100,
onComplete: e => {
e.target.style = '';
},
}}
leave={{opacity: 0, width: 0, scale: 0, duration: 200}}
appear={false}>
{temporaryElements}
{inputVisible && (
<Input
ref={this.saveInputRef}
type="text"
size="small"
style={{width: 120}}
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>
)}
</TweenOneGroup>
</div>
<div>
{tempElements.length > 0 && (
<span>
<Button
onClick={this.handleSave}
htmlType="button" type="primary"
size="small"
disabled={tempElements.length === 0}>
Save
</Button>
<Divider type="vertical"/>
</span>
)}
< Button
onClick={this.handleCloseButton}
size="small">
Cancel
</Button>
</div>
</div>
}
<Divider dashed="true"/>
<div style={{marginTop: 8}}>
<TweenOneGroup
enter={{
scale: 0.8,
opacity: 0,
type: 'from',
duration: 100,
onComplete: e => {
e.target.style = '';
},
}}
leave={{opacity: 0, width: 0, scale: 0, duration: 200}}
appear={false}
>
{tagsElements}
</TweenOneGroup>
</div>
</Spin>
</Card>
< Modal
title="Edit"
visible={this.state.isEditModalVisible}
onCancel={this.closeEditModal}
onOk={this.editItem}
>
<Input value={this.state.editingValue} ref={(input) => this.editingInput = input}
onChange={this.handleEditInputChange}/>
</Modal>
</Col>
</Row>
{isAddNewVisible && (
<div>
<Divider />
<div style={{ marginBottom: 16 }}>
<TweenOneGroup
enter={{
scale: 0.8,
opacity: 0,
type: 'from',
duration: 100,
onComplete: e => {
e.target.style = '';
},
}}
leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}
appear={false}
>
{temporaryElements}
{inputVisible && (
<Input
ref={this.saveInputRef}
type="text"
size="small"
style={{ width: 120 }}
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>
)}
</TweenOneGroup>
</div>
<div>
{tempElements.length > 0 && (
<span>
<Button
onClick={this.handleSave}
htmlType="button"
type="primary"
size="small"
disabled={tempElements.length === 0}
>
Save
</Button>
<Divider type="vertical" />
</span>
)}
<Button onClick={this.handleCloseButton} size="small">
Cancel
</Button>
</div>
</div>
)}
<Divider dashed="true" />
<div style={{ marginTop: 8 }}>
<TweenOneGroup
enter={{
scale: 0.8,
opacity: 0,
type: 'from',
duration: 100,
onComplete: e => {
e.target.style = '';
},
}}
leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}
appear={false}
>
{tagsElements}
</TweenOneGroup>
</div>
);
}
</Spin>
</Card>
<Modal
title="Edit"
visible={this.state.isEditModalVisible}
onCancel={this.closeEditModal}
onOk={this.editItem}
>
<Input
value={this.state.editingValue}
ref={input => (this.editingInput = input)}
onChange={this.handleEditInputChange}
/>
</Modal>
</div>
);
}
}
export default withConfigContext(ManageTags);

@ -16,209 +16,231 @@
* under the License.
*/
import React from "react";
import {
Card,
Button,
Steps,
Row,
Col,
Form,
Result,
notification,
Spin
} from "antd";
import axios from "axios";
import {withRouter} from 'react-router-dom';
import NewAppDetailsForm from "./subForms/NewAppDetailsForm";
import NewAppUploadForm from "./subForms/NewAppUploadForm";
import {withConfigContext} from "../../context/ConfigContext";
import {handleApiError} from "../../js/Utils";
const {Step} = Steps;
import React from 'react';
import { Card, Button, Steps, Row, Col, Form, Result, Spin } from 'antd';
import axios from 'axios';
import { withRouter } from 'react-router-dom';
import NewAppDetailsForm from './subForms/NewAppDetailsForm';
import NewAppUploadForm from './subForms/NewAppUploadForm';
import { withConfigContext } from '../../context/ConfigContext';
import { handleApiError } from '../../js/Utils';
const { Step } = Steps;
class AddNewAppFormComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
current: 0,
categories: [],
tags: [],
icons: [],
screenshots: [],
loading: false,
binaryFiles: [],
application: null,
release: null,
isError: false,
deviceType: null,
supportedOsVersions: [],
errorText: "",
forbiddenErrors: {
supportedOsVersions: false
}
};
constructor(props) {
super(props);
this.state = {
current: 0,
categories: [],
tags: [],
icons: [],
screenshots: [],
loading: false,
binaryFiles: [],
application: null,
release: null,
isError: false,
deviceType: null,
supportedOsVersions: [],
errorText: '',
forbiddenErrors: {
supportedOsVersions: false,
},
};
}
onSuccessApplicationData = application => {
const { formConfig } = this.props;
if (
application.hasOwnProperty('deviceType') &&
formConfig.installationType !== 'WEB_CLIP' &&
formConfig.installationType !== 'CUSTOM'
) {
this.getSupportedOsVersions(application.deviceType);
}
onSuccessApplicationData = (application) => {
const {formConfig} = this.props;
if (application.hasOwnProperty("deviceType") &&
formConfig.installationType !== "WEB_CLIP" &&
formConfig.installationType !== "CUSTOM") {
this.getSupportedOsVersions(application.deviceType);
this.setState({
application,
current: 1,
});
};
onSuccessReleaseData = releaseData => {
const config = this.props.context;
this.setState({
loading: true,
isError: false,
});
const { application } = this.state;
const { data, release } = releaseData;
const { formConfig } = this.props;
const { price } = release;
application.subMethod = price === 0 ? 'FREE' : 'PAID';
// add release wrapper
application[formConfig.releaseWrapperName] = [release];
const json = JSON.stringify(application);
const blob = new Blob([json], {
type: 'application/json',
});
data.append(formConfig.jsonPayloadName, blob);
const url =
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/applications' +
formConfig.endpoint;
axios
.post(url, data)
.then(res => {
if (res.status === 201) {
this.setState({
loading: false,
current: 2,
});
} else {
this.setState({
loading: false,
isError: true,
current: 2,
});
}
})
.catch(error => {
handleApiError(error, error.response.data.data);
this.setState({
application,
current: 1
});
};
onSuccessReleaseData = (releaseData) => {
const config = this.props.context;
this.setState({
loading: true,
isError: false
});
const {application} = this.state;
const {data, release} = releaseData;
const {formConfig} = this.props;
const {price} = release;
application.subMethod = (price === 0) ? "FREE" : "PAID";
//add release wrapper
application[formConfig.releaseWrapperName] = [release];
const json = JSON.stringify(application);
const blob = new Blob([json], {
type: 'application/json'
});
data.append(formConfig.jsonPayloadName, blob);
const url = window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications" + formConfig.endpoint;
axios.post(
url,
data
).then(res => {
if (res.status === 201) {
this.setState({
loading: false,
current: 2
});
} else {
this.setState({
loading: false,
isError: true,
current: 2
});
}
}).catch((error) => {
handleApiError(error, error.response.data.data);
this.setState({
loading: false,
isError: true,
current: 2,
errorText: error.response.data.data
});
loading: false,
isError: true,
current: 2,
errorText: error.response.data.data,
});
};
onClickBackButton = () => {
const current = this.state.current - 1;
this.setState({current});
};
getSupportedOsVersions = (deviceType) => {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt +
`/admin/device-types/${deviceType}/versions`
).then(res => {
if (res.status === 200) {
let supportedOsVersions = JSON.parse(res.data.data);
this.setState({
supportedOsVersions,
loading: false,
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load supported OS versions.", true);
if (error.hasOwnProperty("response") && error.response.status === 403) {
const {forbiddenErrors} = this.state;
forbiddenErrors.supportedOsVersions = true;
this.setState({
forbiddenErrors,
loading: false
})
} else {
this.setState({
loading: false
});
}
});
};
render() {
const {loading, current, isError, supportedOsVersions, errorText, forbiddenErrors} = this.state;
const {formConfig} = this.props;
return (
<div>
<Spin tip="Uploading..." spinning={loading}>
<Row>
<Col span={16} offset={4}>
<Steps style={{minHeight: 32}} current={current}>
<Step key="Application" title="Application"/>
<Step key="Release" title="Release"/>
<Step key="Result" title="Result"/>
</Steps>
<Card style={{marginTop: 24}}>
<div style={{display: (current === 0 ? 'unset' : 'none')}}>
<NewAppDetailsForm
formConfig={formConfig}
onSuccessApplicationData={this.onSuccessApplicationData}/>
</div>
<div style={{display: (current === 1 ? 'unset' : 'none')}}>
<NewAppUploadForm
forbiddenErrors={forbiddenErrors}
formConfig={formConfig}
supportedOsVersions={supportedOsVersions}
onSuccessReleaseData={this.onSuccessReleaseData}
onClickBackButton={this.onClickBackButton}/>
</div>
<div style={{display: (current === 2 ? 'unset' : 'none')}}>
{!isError && (<Result
status="success"
title="Application created successfully!"
extra={[
<Button type="primary" key="console"
onClick={() => this.props.history.push('/publisher/apps')}>
Go to applications
</Button>
]}
/>)}
{isError && (<Result
status="500"
title={errorText}
subTitle="Go back to edit the details and submit again."
extra={<Button onClick={this.onClickBackButton}>Back</Button>}
/>)}
</div>
</Card>
</Col>
</Row>
</Spin>
</div>
});
};
onClickBackButton = () => {
const current = this.state.current - 1;
this.setState({ current });
};
getSupportedOsVersions = deviceType => {
const config = this.props.context;
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.deviceMgt +
`/admin/device-types/${deviceType}/versions`,
)
.then(res => {
if (res.status === 200) {
let supportedOsVersions = JSON.parse(res.data.data);
this.setState({
supportedOsVersions,
loading: false,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load supported OS versions.',
true,
);
}
if (error.hasOwnProperty('response') && error.response.status === 403) {
const { forbiddenErrors } = this.state;
forbiddenErrors.supportedOsVersions = true;
this.setState({
forbiddenErrors,
loading: false,
});
} else {
this.setState({
loading: false,
});
}
});
};
render() {
const {
loading,
current,
isError,
supportedOsVersions,
errorText,
forbiddenErrors,
} = this.state;
const { formConfig } = this.props;
return (
<div>
<Spin tip="Uploading..." spinning={loading}>
<Row>
<Col span={16} offset={4}>
<Steps style={{ minHeight: 32 }} current={current}>
<Step key="Application" title="Application" />
<Step key="Release" title="Release" />
<Step key="Result" title="Result" />
</Steps>
<Card style={{ marginTop: 24 }}>
<div style={{ display: current === 0 ? 'unset' : 'none' }}>
<NewAppDetailsForm
formConfig={formConfig}
onSuccessApplicationData={this.onSuccessApplicationData}
/>
</div>
<div style={{ display: current === 1 ? 'unset' : 'none' }}>
<NewAppUploadForm
forbiddenErrors={forbiddenErrors}
formConfig={formConfig}
supportedOsVersions={supportedOsVersions}
onSuccessReleaseData={this.onSuccessReleaseData}
onClickBackButton={this.onClickBackButton}
/>
</div>
<div style={{ display: current === 2 ? 'unset' : 'none' }}>
{!isError && (
<Result
status="success"
title="Application created successfully!"
extra={[
<Button
type="primary"
key="console"
onClick={() =>
this.props.history.push('/publisher/apps')
}
>
Go to applications
</Button>,
]}
/>
)}
{isError && (
<Result
status="500"
title={errorText}
subTitle="Go back to edit the details and submit again."
extra={
<Button onClick={this.onClickBackButton}>Back</Button>
}
/>
)}
</div>
</Card>
</Col>
</Row>
</Spin>
</div>
);
}
}
const AddNewAppForm = withRouter(Form.create({name: 'add-new-app'})(AddNewAppFormComponent));
const AddNewAppForm = withRouter(
Form.create({ name: 'add-new-app' })(AddNewAppFormComponent),
);
export default withConfigContext(AddNewAppForm);

@ -16,430 +16,481 @@
* under the License.
*/
import React from "react";
import {Alert, Button, Col, Divider, Form, Icon, Input, notification, Row, Select, Spin, Switch, Upload} from "antd";
import axios from "axios";
import {withConfigContext} from "../../../context/ConfigContext";
import {handleApiError} from "../../../js/Utils";
import React from 'react';
import { Alert, Button, Col, Form, Input, Row, Select, Spin } from 'antd';
import axios from 'axios';
import { withConfigContext } from '../../../context/ConfigContext';
import { handleApiError } from '../../../js/Utils';
import debounce from 'lodash.debounce';
const formItemLayout = {
labelCol: {
xs: {span: 24},
sm: {span: 5},
},
wrapperCol: {
xs: {span: 24},
sm: {span: 19},
},
labelCol: {
xs: { span: 24 },
sm: { span: 5 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 19 },
},
};
const {Option} = Select;
const {TextArea} = Input;
const { Option } = Select;
const { TextArea } = Input;
class NewAppDetailsForm extends React.Component {
constructor(props) {
super(props);
this.state = {
categories: [],
tags: [],
deviceTypes: [],
fetching: false,
roleSearchValue: [],
unrestrictedRoles: [],
forbiddenErrors: {
categories: false,
tags: false,
deviceTypes: false,
roles: false,
},
};
this.lastFetchId = 0;
this.fetchRoles = debounce(this.fetchRoles, 800);
}
constructor(props) {
super(props);
this.state = {
categories: [],
tags: [],
deviceTypes: [],
fetching: false,
roleSearchValue: [],
unrestrictedRoles: [],
forbiddenErrors: {
categories: false,
tags: false,
deviceTypes: false,
roles: false
}
};
this.lastFetchId = 0;
this.fetchRoles = debounce(this.fetchRoles, 800);
}
handleSubmit = e => {
e.preventDefault();
const {formConfig} = this.props;
const {specificElements} = formConfig;
this.props.form.validateFields((err, values) => {
if (!err) {
this.setState({
loading: true
});
const {name, description, categories, tags, unrestrictedRoles} = values;
const unrestrictedRolesData = [];
unrestrictedRoles.map(val => {
unrestrictedRolesData.push(val.key);
});
const application = {
name,
description,
categories,
tags,
unrestrictedRoles: unrestrictedRolesData,
};
if (formConfig.installationType !== "WEB_CLIP") {
application.deviceType = values.deviceType;
} else {
application.type = "WEB_CLIP";
application.deviceType = "ALL";
}
handleSubmit = e => {
e.preventDefault();
const { formConfig } = this.props;
this.props.onSuccessApplicationData(application);
}
this.props.form.validateFields((err, values) => {
if (!err) {
this.setState({
loading: true,
});
};
const {
name,
description,
categories,
tags,
unrestrictedRoles,
} = values;
const unrestrictedRolesData = [];
unrestrictedRoles.map(val => {
unrestrictedRolesData.push(val.key);
});
const application = {
name,
description,
categories,
tags,
unrestrictedRoles: unrestrictedRolesData,
};
componentDidMount() {
this.getCategories();
this.getTags();
this.getDeviceTypes();
}
if (formConfig.installationType !== 'WEB_CLIP') {
application.deviceType = values.deviceType;
} else {
application.type = 'WEB_CLIP';
application.deviceType = 'ALL';
}
getCategories = () => {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications/categories"
).then(res => {
if (res.status === 200) {
let categories = JSON.parse(res.data.data);
this.setState({
categories: categories,
loading: false
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load categories.", true);
if (error.hasOwnProperty("response") && error.response.status === 403) {
const {forbiddenErrors} = this.state;
forbiddenErrors.categories = true;
this.setState({
forbiddenErrors,
loading: false
})
} else {
this.setState({
loading: false
});
}
});
};
this.props.onSuccessApplicationData(application);
}
});
};
getTags = () => {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications/tags"
).then(res => {
if (res.status === 200) {
let tags = JSON.parse(res.data.data);
this.setState({
tags: tags,
loading: false,
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load tags.", true);
if (error.hasOwnProperty("response") && error.response.status === 403) {
const {forbiddenErrors} = this.state;
forbiddenErrors.tags = true;
this.setState({
forbiddenErrors,
loading: false
})
} else {
this.setState({
loading: false
});
}
});
};
componentDidMount() {
this.getCategories();
this.getTags();
this.getDeviceTypes();
}
getDeviceTypes = () => {
const config = this.props.context;
const {formConfig} = this.props;
const {installationType} = formConfig;
getCategories = () => {
const config = this.props.context;
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/applications/categories',
)
.then(res => {
if (res.status === 200) {
let categories = JSON.parse(res.data.data);
this.setState({
categories: categories,
loading: false,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load categories.',
true,
);
if (error.hasOwnProperty('response') && error.response.status === 403) {
const { forbiddenErrors } = this.state;
forbiddenErrors.categories = true;
this.setState({
forbiddenErrors,
loading: false,
});
} else {
this.setState({
loading: false,
});
}
});
};
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt + "/device-types"
).then(res => {
if (res.status === 200) {
const allDeviceTypes = JSON.parse(res.data.data);
const mobileDeviceTypes = config.deviceTypes.mobileTypes;
const allowedDeviceTypes = [];
getTags = () => {
const config = this.props.context;
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/applications/tags',
)
.then(res => {
if (res.status === 200) {
let tags = JSON.parse(res.data.data);
this.setState({
tags: tags,
loading: false,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load tags.',
true,
);
if (error.hasOwnProperty('response') && error.response.status === 403) {
const { forbiddenErrors } = this.state;
forbiddenErrors.tags = true;
this.setState({
forbiddenErrors,
loading: false,
});
} else {
this.setState({
loading: false,
});
}
});
};
// exclude mobile device types if installation type is custom
if (installationType === "CUSTOM") {
allDeviceTypes.forEach(deviceType => {
if (!mobileDeviceTypes.includes(deviceType.name)) {
allowedDeviceTypes.push(deviceType);
}
});
} else {
allDeviceTypes.forEach(deviceType => {
if (mobileDeviceTypes.includes(deviceType.name)) {
allowedDeviceTypes.push(deviceType);
}
});
}
getDeviceTypes = () => {
const config = this.props.context;
const { formConfig } = this.props;
const { installationType } = formConfig;
this.setState({
deviceTypes: allowedDeviceTypes,
loading: false,
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load device types.", true);
if (error.hasOwnProperty("response") && error.response.status === 403) {
const {forbiddenErrors} = this.state;
forbiddenErrors.deviceTypes = true;
this.setState({
forbiddenErrors,
loading: false
})
} else {
this.setState({
loading: false
});
}
});
};
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.deviceMgt +
'/device-types',
)
.then(res => {
if (res.status === 200) {
const allDeviceTypes = JSON.parse(res.data.data);
const mobileDeviceTypes = config.deviceTypes.mobileTypes;
const allowedDeviceTypes = [];
fetchRoles = value => {
const config = this.props.context;
this.lastFetchId += 1;
const fetchId = this.lastFetchId;
this.setState({data: [], fetching: true});
// exclude mobile device types if installation type is custom
if (installationType === 'CUSTOM') {
allDeviceTypes.forEach(deviceType => {
if (!mobileDeviceTypes.includes(deviceType.name)) {
allowedDeviceTypes.push(deviceType);
}
});
} else {
allDeviceTypes.forEach(deviceType => {
if (mobileDeviceTypes.includes(deviceType.name)) {
allowedDeviceTypes.push(deviceType);
}
});
}
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;
}
this.setState({
deviceTypes: allowedDeviceTypes,
loading: false,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load device types.',
true,
);
if (error.hasOwnProperty('response') && error.response.status === 403) {
const { forbiddenErrors } = this.state;
forbiddenErrors.deviceTypes = true;
this.setState({
forbiddenErrors,
loading: false,
});
} else {
this.setState({
loading: false,
});
}
});
};
const data = res.data.data.roles.map(role => ({
text: role,
value: role,
}));
fetchRoles = value => {
const config = this.props.context;
this.lastFetchId += 1;
const fetchId = this.lastFetchId;
this.setState({ data: [], fetching: true });
this.setState({
unrestrictedRoles: data,
fetching: false
});
}
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;
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load roles.", true);
if (error.hasOwnProperty("response") && error.response.status === 403) {
const {forbiddenErrors} = this.state;
forbiddenErrors.roles = true;
this.setState({
forbiddenErrors,
fetching: false
})
} else {
this.setState({
fetching: false
});
}
});
};
const data = res.data.data.roles.map(role => ({
text: role,
value: role,
}));
handleRoleSearch = roleSearchValue => {
this.setState({
roleSearchValue,
unrestrictedRoles: [],
this.setState({
unrestrictedRoles: data,
fetching: false,
});
};
render() {
const {formConfig} = this.props;
const {categories, tags, deviceTypes, fetching, roleSearchValue, unrestrictedRoles, forbiddenErrors} = this.state;
const {getFieldDecorator} = this.props.form;
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load roles.',
true,
);
if (error.hasOwnProperty('response') && error.response.status === 403) {
const { forbiddenErrors } = this.state;
forbiddenErrors.roles = true;
this.setState({
forbiddenErrors,
fetching: false,
});
} else {
this.setState({
fetching: false,
});
}
});
};
return (
<div>
<Row>
<Col md={5}>
handleRoleSearch = roleSearchValue => {
this.setState({
roleSearchValue,
unrestrictedRoles: [],
fetching: false,
});
};
</Col>
<Col md={14}>
<Form
labelAlign="right"
layout="horizontal"
onSubmit={this.handleSubmit}>
{formConfig.installationType !== "WEB_CLIP" && (
<div>
{(forbiddenErrors.deviceTypes) && (
<Alert
message="You don't have permission to view device types."
type="warning"
banner
closable/>
)}
<Form.Item {...formItemLayout} label="Device Type">
{getFieldDecorator('deviceType', {
rules: [
{
required: true,
message: 'Please select device type'
}
],
}
)(
<Select
style={{width: '100%'}}
placeholder="select device type">
{
deviceTypes.map(deviceType => {
return (
<Option
key={deviceType.name}>
{deviceType.name}
</Option>
)
})
}
</Select>
)}
</Form.Item>
</div>
)}
render() {
const { formConfig } = this.props;
const {
categories,
tags,
deviceTypes,
fetching,
unrestrictedRoles,
forbiddenErrors,
} = this.state;
const { getFieldDecorator } = this.props.form;
{/*app name*/}
<Form.Item {...formItemLayout} label="App Name">
{getFieldDecorator('name', {
rules: [{
required: true,
message: 'Please input a name'
}],
})(
<Input placeholder="ex: Lorem App"/>
)}
</Form.Item>
return (
<div>
<Row>
<Col md={5}></Col>
<Col md={14}>
<Form
labelAlign="right"
layout="horizontal"
onSubmit={this.handleSubmit}
>
{formConfig.installationType !== 'WEB_CLIP' && (
<div>
{forbiddenErrors.deviceTypes && (
<Alert
message="You don't have permission to view device types."
type="warning"
banner
closable
/>
)}
<Form.Item {...formItemLayout} label="Device Type">
{getFieldDecorator('deviceType', {
rules: [
{
required: true,
message: 'Please select device type',
},
],
})(
<Select
style={{ width: '100%' }}
placeholder="select device type"
>
{deviceTypes.map(deviceType => {
return (
<Option key={deviceType.name}>
{deviceType.name}
</Option>
);
})}
</Select>,
)}
</Form.Item>
</div>
)}
{/*description*/}
<Form.Item {...formItemLayout} label="Description">
{getFieldDecorator('description', {
rules: [{
required: true,
message: 'Please enter a description'
}],
})(
<TextArea placeholder="Enter the description..." rows={7}/>
)}
</Form.Item>
{/* app name*/}
<Form.Item {...formItemLayout} label="App Name">
{getFieldDecorator('name', {
rules: [
{
required: true,
message: 'Please input a name',
},
],
})(<Input placeholder="ex: Lorem App" />)}
</Form.Item>
{/*Unrestricted Roles*/}
{(forbiddenErrors.roles) && (
<Alert
message="You don't have permission to view roles."
type="warning"
banner
closable/>
)}
<Form.Item {...formItemLayout} label="Visible Roles">
{getFieldDecorator('unrestrictedRoles', {
rules: [],
initialValue: []
})(
<Select
mode="multiple"
labelInValue
// value={roleSearchValue}
placeholder="Search roles"
notFoundContent={fetching ? <Spin size="small"/> : null}
filterOption={false}
onSearch={this.fetchRoles}
onChange={this.handleRoleSearch}
style={{width: '100%'}}>
{unrestrictedRoles.map(d => (
<Option key={d.value}>{d.text}</Option>
))}
</Select>
)}
</Form.Item>
{(forbiddenErrors.categories) && (
<Alert
message="You don't have permission to view categories."
type="warning"
banner
closable/>
)}
<Form.Item {...formItemLayout} label="Categories">
{getFieldDecorator('categories', {
rules: [{
required: true,
message: 'Please select categories'
}],
})(
<Select
mode="multiple"
style={{width: '100%'}}
placeholder="Select a Category"
onChange={this.handleCategoryChange}>
{
categories.map(category => {
return (
<Option
key={category.categoryName}>
{category.categoryName}
</Option>
)
})
}
</Select>
)}
</Form.Item>
{(forbiddenErrors.tags) && (
<Alert
message="You don't have permission to view tags."
type="warning"
banner
closable/>
)}
<Form.Item {...formItemLayout} label="Tags">
{getFieldDecorator('tags', {
rules: [{
required: true,
message: 'Please select tags'
}],
})(
<Select
mode="tags"
style={{width: '100%'}}
placeholder="Tags">
{
tags.map(tag => {
return (
<Option
key={tag.tagName}>
{tag.tagName}
</Option>
)
})
}
</Select>
)}
</Form.Item>
<Form.Item style={{float: "right"}}>
<Button type="primary" htmlType="submit">
Next
</Button>
</Form.Item>
</Form>
</Col>
</Row>
</div>
);
}
{/* description*/}
<Form.Item {...formItemLayout} label="Description">
{getFieldDecorator('description', {
rules: [
{
required: true,
message: 'Please enter a description',
},
],
})(
<TextArea placeholder="Enter the description..." rows={7} />,
)}
</Form.Item>
{/* Unrestricted Roles*/}
{forbiddenErrors.roles && (
<Alert
message="You don't have permission to view roles."
type="warning"
banner
closable
/>
)}
<Form.Item {...formItemLayout} label="Visible Roles">
{getFieldDecorator('unrestrictedRoles', {
rules: [],
initialValue: [],
})(
<Select
mode="multiple"
labelInValue
// value={roleSearchValue}
placeholder="Search roles"
notFoundContent={fetching ? <Spin size="small" /> : null}
filterOption={false}
onSearch={this.fetchRoles}
onChange={this.handleRoleSearch}
style={{ width: '100%' }}
>
{unrestrictedRoles.map(d => (
<Option key={d.value}>{d.text}</Option>
))}
</Select>,
)}
</Form.Item>
{forbiddenErrors.categories && (
<Alert
message="You don't have permission to view categories."
type="warning"
banner
closable
/>
)}
<Form.Item {...formItemLayout} label="Categories">
{getFieldDecorator('categories', {
rules: [
{
required: true,
message: 'Please select categories',
},
],
})(
<Select
mode="multiple"
style={{ width: '100%' }}
placeholder="Select a Category"
onChange={this.handleCategoryChange}
>
{categories.map(category => {
return (
<Option key={category.categoryName}>
{category.categoryName}
</Option>
);
})}
</Select>,
)}
</Form.Item>
{forbiddenErrors.tags && (
<Alert
message="You don't have permission to view tags."
type="warning"
banner
closable
/>
)}
<Form.Item {...formItemLayout} label="Tags">
{getFieldDecorator('tags', {
rules: [
{
required: true,
message: 'Please select tags',
},
],
})(
<Select
mode="tags"
style={{ width: '100%' }}
placeholder="Tags"
>
{tags.map(tag => {
return <Option key={tag.tagName}>{tag.tagName}</Option>;
})}
</Select>,
)}
</Form.Item>
<Form.Item style={{ float: 'right' }}>
<Button type="primary" htmlType="submit">
Next
</Button>
</Form.Item>
</Form>
</Col>
</Row>
</div>
);
}
}
export default withConfigContext(Form.create({name: 'app-details-form'})(NewAppDetailsForm));
export default withConfigContext(
Form.create({ name: 'app-details-form' })(NewAppDetailsForm),
);

@ -16,146 +16,162 @@
* under the License.
*/
import React from "react";
import {Form, notification, Spin, Card, Row, Col} from "antd";
import axios from "axios";
import {withRouter} from 'react-router-dom'
import {withConfigContext} from "../../context/ConfigContext";
import {handleApiError} from "../../js/Utils";
import NewAppUploadForm from "../new-app/subForms/NewAppUploadForm";
import React from 'react';
import { Form, notification, Spin, Card, Row, Col } from 'antd';
import axios from 'axios';
import { withRouter } from 'react-router-dom';
import { withConfigContext } from '../../context/ConfigContext';
import { handleApiError } from '../../js/Utils';
import NewAppUploadForm from '../new-app/subForms/NewAppUploadForm';
const formConfig = {
specificElements: {
binaryFile: {
required: true
}
}
specificElements: {
binaryFile: {
required: true,
},
},
};
class AddNewReleaseFormComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
supportedOsVersions: [],
application: null,
release: null,
deviceType: null,
forbiddenErrors: {
supportedOsVersions: false
}
};
}
componentDidMount() {
this.getSupportedOsVersions(this.props.deviceType);
}
getSupportedOsVersions = (deviceType) => {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt +
`/admin/device-types/${deviceType}/versions`
).then(res => {
if (res.status === 200) {
let supportedOsVersions = JSON.parse(res.data.data);
this.setState({
supportedOsVersions,
loading: false,
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load supported OS versions.", true);
if (error.hasOwnProperty("response") && error.response.status === 403) {
const {forbiddenErrors} = this.state;
forbiddenErrors.supportedOsVersions = true;
this.setState({
forbiddenErrors,
loading: false
})
} else {
this.setState({
loading: false
});
}
});
constructor(props) {
super(props);
this.state = {
loading: false,
supportedOsVersions: [],
application: null,
release: null,
deviceType: null,
forbiddenErrors: {
supportedOsVersions: false,
},
};
}
onSuccessReleaseData = (releaseData) => {
const config = this.props.context;
const {appId, deviceType} = this.props;
this.setState({
loading: true
});
const {data, release} = releaseData;
componentDidMount() {
this.getSupportedOsVersions(this.props.deviceType);
}
const json = JSON.stringify(release);
const blob = new Blob([json], {
type: 'application/json'
});
data.append("applicationRelease", blob);
getSupportedOsVersions = deviceType => {
const config = this.props.context;
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.deviceMgt +
`/admin/device-types/${deviceType}/versions`,
)
.then(res => {
if (res.status === 200) {
let supportedOsVersions = JSON.parse(res.data.data);
this.setState({
supportedOsVersions,
loading: false,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load supported OS versions.',
true,
);
if (error.hasOwnProperty('response') && error.response.status === 403) {
const { forbiddenErrors } = this.state;
forbiddenErrors.supportedOsVersions = true;
this.setState({
forbiddenErrors,
loading: false,
});
} else {
this.setState({
loading: false,
});
}
});
};
const url = window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher +
"/applications/" + deviceType + "/ent-app/" + appId;
axios.post(
url,
data
).then(res => {
if (res.status === 201) {
this.setState({
loading: false,
});
onSuccessReleaseData = releaseData => {
const config = this.props.context;
const { appId, deviceType } = this.props;
this.setState({
loading: true,
});
const { data, release } = releaseData;
notification["success"]({
message: "Done!",
description:
"New release was added successfully",
});
const uuid = res.data.data.uuid;
this.props.history.push('/publisher/apps/releases/' + uuid);
const json = JSON.stringify(release);
const blob = new Blob([json], {
type: 'application/json',
});
data.append('applicationRelease', blob);
} else {
this.setState({
loading: false
});
}
}).catch((error) => {
handleApiError(error, "Sorry, we were unable to complete your request.");
this.setState({
loading: false
});
});
const url =
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/applications/' +
deviceType +
'/ent-app/' +
appId;
axios
.post(url, data)
.then(res => {
if (res.status === 201) {
this.setState({
loading: false,
});
};
notification.success({
message: 'Done!',
description: 'New release was added successfully',
});
const uuid = res.data.data.uuid;
this.props.history.push('/publisher/apps/releases/' + uuid);
} else {
this.setState({
loading: false,
});
}
})
.catch(error => {
handleApiError(
error,
'Sorry, we were unable to complete your request.',
);
this.setState({
loading: false,
});
});
};
onClickBackButton = () => {
this.props.history.push('/publisher/apps/');
};
onClickBackButton = () => {
this.props.history.push('/publisher/apps/');
};
render() {
const {loading, supportedOsVersions, forbiddenErrors} = this.state;
return (
<div>
<Spin tip="Uploading..." spinning={loading}>
<Row>
<Col span={17} offset={4}>
<Card>
<NewAppUploadForm
forbiddenErrors={forbiddenErrors}
formConfig={formConfig}
supportedOsVersions={supportedOsVersions}
onSuccessReleaseData={this.onSuccessReleaseData}
onClickBackButton={this.onClickBackButton}
/>
</Card>
</Col>
</Row>
</Spin>
</div>
);
}
render() {
const { loading, supportedOsVersions, forbiddenErrors } = this.state;
return (
<div>
<Spin tip="Uploading..." spinning={loading}>
<Row>
<Col span={17} offset={4}>
<Card>
<NewAppUploadForm
forbiddenErrors={forbiddenErrors}
formConfig={formConfig}
supportedOsVersions={supportedOsVersions}
onSuccessReleaseData={this.onSuccessReleaseData}
onClickBackButton={this.onClickBackButton}
/>
</Card>
</Col>
</Row>
</Spin>
</div>
);
}
}
const AddReleaseForm = withRouter(Form.create({name: 'add-new-release'})(AddNewReleaseFormComponent));
const AddReleaseForm = withRouter(
Form.create({ name: 'add-new-release' })(AddNewReleaseFormComponent),
);
export default withConfigContext(AddReleaseForm);

@ -16,19 +16,19 @@
* under the License.
*/
import React from "react";
import React from 'react';
const ConfigContext = React.createContext();
export const withConfigContext = Component => {
return props => (
<ConfigContext.Consumer>
{context => {
return <Component {...props} context={context}/>;
}}
</ConfigContext.Consumer>
);
// eslint-disable-next-line react/display-name
return props => (
<ConfigContext.Consumer>
{context => {
return <Component {...props} context={context} />;
}}
</ConfigContext.Consumer>
);
};
export default ConfigContext;

@ -19,91 +19,87 @@
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 AddNewEnterpriseApp from "./pages/dashboard/add-new-app/AddNewEnterpriseApp";
import Mange from "./pages/dashboard/manage/Manage";
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 AddNewEnterpriseApp from './pages/dashboard/add-new-app/AddNewEnterpriseApp';
import Mange from './pages/dashboard/manage/Manage';
import './index.css';
import AddNewPublicApp from "./pages/dashboard/add-new-app/AddNewPublicApp";
import AddNewWebClip from "./pages/dashboard/add-new-app/AddNewWebClip";
import AddNewRelease from "./pages/dashboard/add-new-release/AddNewRelease";
import AddNewCustomApp from "./pages/dashboard/add-new-app/AddNewCustomApp";
import ManageAndroidEnterprise from "./pages/dashboard/manage/android-enterprise/ManageAndroidEnterprise";
import Page from "./pages/dashboard/manage/android-enterprise/page/Page";
import AddNewPublicApp from './pages/dashboard/add-new-app/AddNewPublicApp';
import AddNewWebClip from './pages/dashboard/add-new-app/AddNewWebClip';
import AddNewRelease from './pages/dashboard/add-new-release/AddNewRelease';
import AddNewCustomApp from './pages/dashboard/add-new-app/AddNewCustomApp';
import ManageAndroidEnterprise from './pages/dashboard/manage/android-enterprise/ManageAndroidEnterprise';
import Page from './pages/dashboard/manage/android-enterprise/page/Page';
const routes = [
{
path: '/publisher/login',
{
path: '/publisher/login',
exact: true,
component: Login,
},
{
path: '/publisher/',
exact: false,
component: Dashboard,
routes: [
{
path: '/publisher/apps',
component: Apps,
exact: true,
},
{
path: '/publisher/apps/releases/:uuid',
exact: true,
component: Release,
},
{
path: '/publisher/apps/:deviceType/:appId/add-release',
component: AddNewRelease,
exact: true,
},
{
path: '/publisher/add-new-app/enterprise',
component: AddNewEnterpriseApp,
exact: true,
},
{
path: '/publisher/add-new-app/public',
component: AddNewPublicApp,
exact: true,
},
{
path: '/publisher/add-new-app/web-clip',
component: AddNewWebClip,
exact: true,
component: Login
},
{
path: '/publisher/',
exact: false,
component: Dashboard,
routes: [
{
path: '/publisher/apps',
component: Apps,
exact: true
},
{
path: '/publisher/apps/releases/:uuid',
exact: true,
component: Release
},
{
path: '/publisher/apps/:deviceType/:appId/add-release',
component: AddNewRelease,
exact: true
},
{
path: '/publisher/add-new-app/enterprise',
component: AddNewEnterpriseApp,
exact: true
},
{
path: '/publisher/add-new-app/public',
component: AddNewPublicApp,
exact: true
},
{
path: '/publisher/add-new-app/web-clip',
component: AddNewWebClip,
exact: true
},
{
path: '/publisher/add-new-app/custom-app',
component: AddNewCustomApp,
exact: true
},
{
path: '/publisher/manage',
component: Mange,
exact: true
},
{
path: '/publisher/manage/android-enterprise',
component: ManageAndroidEnterprise,
exact: true
},
{
path: '/publisher/manage/android-enterprise/pages/:pageName/:pageId',
component: Page,
exact: true
}
]
}
},
{
path: '/publisher/add-new-app/custom-app',
component: AddNewCustomApp,
exact: true,
},
{
path: '/publisher/manage',
component: Mange,
exact: true,
},
{
path: '/publisher/manage/android-enterprise',
component: ManageAndroidEnterprise,
exact: true,
},
{
path: '/publisher/manage/android-enterprise/pages/:pageName/:pageId',
component: Page,
exact: true,
},
],
},
];
ReactDOM.render(
<App routes={routes}/>,
document.getElementById('root'));
ReactDOM.render(<App routes={routes} />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.

@ -16,18 +16,29 @@
* under the License.
*/
import {notification} from "antd";
import { notification } from 'antd';
export const handleApiError = (error, message, isForbiddenMessageSilent = false) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
const redirectUrl = encodeURI(window.location.href);
window.location.href = window.location.origin + `/publisher/login?redirect=${redirectUrl}`;
// silence 403 forbidden message
} else if (!(isForbiddenMessageSilent && error.hasOwnProperty("response") && error.response.status === 403)) {
notification["error"]({
message: "There was a problem",
duration: 10,
description: message,
});
}
export const handleApiError = (
error,
message,
isForbiddenMessageSilent = false,
) => {
if (error.hasOwnProperty('response') && error.response.status === 401) {
const redirectUrl = encodeURI(window.location.href);
window.location.href =
window.location.origin + `/publisher/login?redirect=${redirectUrl}`;
// silence 403 forbidden message
} else if (
!(
isForbiddenMessageSilent &&
error.hasOwnProperty('response') &&
error.response.status === 403
)
) {
notification.error({
message: 'There was a problem',
duration: 10,
description: message,
});
}
};

@ -16,160 +16,183 @@
* under the License.
*/
import React from "react";
import {Typography, Row, Col, Form, Icon, Input, Button, Checkbox, message, notification} from 'antd';
import React from 'react';
import {
Typography,
Row,
Col,
Form,
Icon,
Input,
Button,
message,
notification,
} from 'antd';
import './Login.css';
import axios from 'axios';
import "./Login.css";
import {withConfigContext} from "../context/ConfigContext";
import {handleApiError} from "../js/Utils";
import './Login.css';
import { withConfigContext } from '../context/ConfigContext';
const {Title} = Typography;
const {Text} = Typography;
const { Title } = Typography;
const { Text } = Typography;
class Login extends React.Component {
render() {
const config = this.props.context;
return (
<div className="login">
<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>
);
}
render() {
const config = this.props.context;
return (
<div className="login">
<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,
};
}
constructor(props) {
super(props);
this.state = {
inValid: false,
loading: false
handleSubmit = e => {
const config = this.props.context;
const thisForm = this;
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: 'publisher',
};
}
handleSubmit = (e) => {
const config = this.props.context;
const thisForm = this;
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: "publisher"
};
const request = Object.keys(parameters)
.map(key => key + '=' + parameters[key])
.join('&');
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) {
let redirectUrl = window.location.origin+"/publisher";
const searchParams = new URLSearchParams(window.location.search);
if(searchParams.has("redirect")){
redirectUrl = searchParams.get("redirect");
}
window.location = redirectUrl;
}
}).catch(function (error) {
if (error.hasOwnProperty("response") && error.response.status === 401) {
thisForm.setState({
loading: false,
inValid: true
});
} else {
notification["error"]({
message: "There was a problem",
duration: 10,
description: message,
});
thisForm.setState({
loading: false,
inValid: false
});
}
});
axios
.post(window.location.origin + config.serverConfig.loginUri, request)
.then(res => {
if (res.status === 200) {
let redirectUrl = window.location.origin + '/publisher';
const searchParams = new URLSearchParams(window.location.search);
if (searchParams.has('redirect')) {
redirectUrl = searchParams.get('redirect');
}
window.location = redirectUrl;
}
})
.catch(function(error) {
if (
error.hasOwnProperty('response') &&
error.response.status === 401
) {
thisForm.setState({
loading: false,
inValid: true,
});
} else {
notification.error({
message: 'There was a problem',
duration: 10,
description: message,
});
thisForm.setState({
loading: false,
inValid: 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 enter your username'}],
})(
<Input 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 enter your password'}],
})(
<Input style={{height: 32}}
prefix={<Icon type="lock" style={{color: 'rgba(0,0,0,.25)'}}/>} type="password"
placeholder="Password"/>
)}
</Form.Item>
{loading}
{errorMsg}
<Form.Item>
<Button loading={this.state.loading} block type="primary" htmlType="submit" className="login-form-button">
Log in
</Button>
</Form.Item>
</Form>
);
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 enter your username' }],
})(
<Input
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 enter your password' }],
})(
<Input
style={{ height: 32 }}
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
type="password"
placeholder="Password"
/>,
)}
</Form.Item>
{loading}
{errorMsg}
<Form.Item>
<Button
loading={this.state.loading}
block
type="primary"
htmlType="submit"
className="login-form-button"
>
Log in
</Button>
</Form.Item>
</Form>
);
}
}
const WrappedNormalLoginForm = Form.create({name: 'normal_login'})(withConfigContext(NormalLoginForm));
const WrappedNormalLoginForm = Form.create({ name: 'normal_login' })(
withConfigContext(NormalLoginForm),
);
export default withConfigContext(Login);

@ -16,214 +16,247 @@
* under the License.
*/
import React from "react";
import {Layout, Menu, Icon, Drawer, Button} from 'antd';
import {Switch, Link} from "react-router-dom";
import RouteWithSubRoutes from "../../components/RouteWithSubRoutes"
import {Redirect} from 'react-router'
import "./Dashboard.css";
import {withConfigContext} from "../../context/ConfigContext";
import Logout from "./logout/Logout";
import React from 'react';
import { Layout, Menu, Icon, Drawer, Button } from 'antd';
import { Switch, Link } from 'react-router-dom';
import RouteWithSubRoutes from '../../components/RouteWithSubRoutes';
import { Redirect } from 'react-router';
import './Dashboard.css';
import { withConfigContext } from '../../context/ConfigContext';
import Logout from './logout/Logout';
const {Header, Content, Footer} = Layout;
const {SubMenu} = Menu;
const { Header, Content, Footer } = Layout;
const { SubMenu } = Menu;
class Dashboard extends React.Component {
constructor(props) {
super(props);
this.state = {
routes: props.routes,
visible: false,
collapsed: false
};
this.config = this.props.context;
this.Logo = this.config.theme.logo;
this.footerText = this.config.theme.footerText;
}
showMobileNavigationBar = () => {
this.setState({
visible: true,
collapsed: !this.state.collapsed
});
};
onCloseMobileNavigationBar = () => {
this.setState({
visible: false,
});
constructor(props) {
super(props);
this.state = {
routes: props.routes,
visible: false,
collapsed: false,
};
this.config = this.props.context;
this.Logo = this.config.theme.logo;
this.footerText = this.config.theme.footerText;
}
render() {
return (
<div>
<Layout>
<Header style={{paddingLeft: 0, paddingRight: 0, backgroundColor: "white"}}>
<div className="logo-image">
<Link to="/publisher/apps"><img alt="logo" src={this.Logo}/></Link>
</div>
showMobileNavigationBar = () => {
this.setState({
visible: true,
collapsed: !this.state.collapsed,
});
};
<div className="web-layout">
<Menu
theme="light"
mode="horizontal"
defaultSelectedKeys={['1']}
style={{lineHeight: '64px'}}>
<Menu.Item key="1"><Link to="/publisher/apps"><Icon
type="appstore"/>Apps</Link></Menu.Item>
onCloseMobileNavigationBar = () => {
this.setState({
visible: false,
});
};
<SubMenu
title={
<span className="submenu-title-wrapper">
<Icon type="plus"/>
Add New App
</span>
}>
<Menu.Item key="add-new-public-app">
<Link to="/publisher/add-new-app/public">
Public App
</Link>
</Menu.Item>
<Menu.Item key="add-new-enterprise-app">
<Link to="/publisher/add-new-app/enterprise">
Enterprise App
</Link>
</Menu.Item>
<Menu.Item key="add-new-web-clip">
<Link to="/publisher/add-new-app/web-clip">
Web Clip
</Link>
</Menu.Item>
<Menu.Item key="add-new-custom-app">
<Link to="/publisher/add-new-app/custom-app">
Custom App
</Link>
</Menu.Item>
</SubMenu>
render() {
return (
<div>
<Layout>
<Header
style={{
paddingLeft: 0,
paddingRight: 0,
backgroundColor: 'white',
}}
>
<div className="logo-image">
<Link to="/publisher/apps">
<img alt="logo" src={this.Logo} />
</Link>
</div>
<SubMenu
title={
<span className="submenu-title-wrapper">
<Icon type="control"/>Manage
</span>}>
<Menu.Item key="manage">
<Link to="/publisher/manage">
<Icon type="setting"/> General
</Link>
</Menu.Item>
{this.config.androidEnterpriseToken != null && (
<Menu.Item key="manage-android-enterprise">
<Link to="/publisher/manage/android-enterprise">
<Icon type="android" theme="filled"/> Android Enterprise
</Link>
</Menu.Item>
)}
</SubMenu>
<div className="web-layout">
<Menu
theme="light"
mode="horizontal"
defaultSelectedKeys={['1']}
style={{ lineHeight: '64px' }}
>
<Menu.Item key="1">
<Link to="/publisher/apps">
<Icon type="appstore" />
Apps
</Link>
</Menu.Item>
<SubMenu className="profile"
title={
<span className="submenu-title-wrapper">
<Icon type="user"/>{this.config.user}
</span>}>
<Logout/>
</SubMenu>
</Menu>
</div>
</Header>
</Layout>
<SubMenu
title={
<span className="submenu-title-wrapper">
<Icon type="plus" />
Add New App
</span>
}
>
<Menu.Item key="add-new-public-app">
<Link to="/publisher/add-new-app/public">Public App</Link>
</Menu.Item>
<Menu.Item key="add-new-enterprise-app">
<Link to="/publisher/add-new-app/enterprise">
Enterprise App
</Link>
</Menu.Item>
<Menu.Item key="add-new-web-clip">
<Link to="/publisher/add-new-app/web-clip">Web Clip</Link>
</Menu.Item>
<Menu.Item key="add-new-custom-app">
<Link to="/publisher/add-new-app/custom-app">
Custom App
</Link>
</Menu.Item>
</SubMenu>
<Layout className="mobile-layout">
<div className="mobile-menu-button">
<Button type="link" onClick={this.showMobileNavigationBar}>
<Icon
type={this.state.collapsed ? 'menu-fold' : 'menu-unfold'}
className="bar-icon"/>
</Button>
</div>
</Layout>
<Drawer
title={
<Link to="/publisher/apps" onClick={this.onCloseMobileNavigationBar}>
<img alt="logo"
src={this.Logo}
style={{marginLeft: 30}}
width={"60%"}/>
</Link>
}
placement="left"
closable={false}
onClose={this.onCloseMobileNavigationBar}
visible={this.state.visible}
getContainer={false}
style={{position: 'absolute'}}>
<Menu
theme="light"
mode="inline"
defaultSelectedKeys={['1']}
style={{lineHeight: '64px', width: 231}}
onClick={this.onCloseMobileNavigationBar}>
<Menu.Item key="1">
<Link to="/publisher/apps">
<Icon type="appstore"/>Apps
</Link>
</Menu.Item>
<SubMenu
title={
<span className="submenu-title-wrapper">
<Icon type="plus"/>Add New App
</span>
}>
<Menu.Item key="setting:1">
<Link to="/publisher/add-new-app/public">Public APP</Link>
</Menu.Item>
<Menu.Item key="setting:2">
<Link to="/publisher/add-new-app/enterprise">Enterprise APP</Link>
</Menu.Item>
<Menu.Item key="setting:3">
<Link to="/publisher/add-new-app/web-clip">Web Clip</Link>
</Menu.Item>
<Menu.Item key="setting:4">
<Link to="/publisher/add-new-app/custom-app">Custom App</Link>
</Menu.Item>
</SubMenu>
<Menu.Item key="2">
<Link to="/publisher/manage">
<Icon type="control"/>Manage
</Link>
</Menu.Item>
</Menu>
</Drawer>
<Layout className="mobile-layout">
<Menu
mode="horizontal"
defaultSelectedKeys={['1']}
style={{lineHeight: '63px', position: 'fixed', marginLeft: '80%'}}>
<SubMenu
title={
<span className="submenu-title-wrapper">
<Icon type="user"/>
</span>}>
<Logout/>
</SubMenu>
</Menu>
</Layout>
<SubMenu
title={
<span className="submenu-title-wrapper">
<Icon type="control" />
Manage
</span>
}
>
<Menu.Item key="manage">
<Link to="/publisher/manage">
<Icon type="setting" /> General
</Link>
</Menu.Item>
{this.config.androidEnterpriseToken != null && (
<Menu.Item key="manage-android-enterprise">
<Link to="/publisher/manage/android-enterprise">
<Icon type="android" theme="filled" /> Android
Enterprise
</Link>
</Menu.Item>
)}
</SubMenu>
<Layout className="dashboard-body">
<Content style={{marginTop: 2}}>
<Switch>
<Redirect exact from="/publisher" to="/publisher/apps"/>
{this.state.routes.map((route) => (
<RouteWithSubRoutes key={route.path} {...route} />
))}
</Switch>
</Content>
<Footer style={{textAlign: 'center'}}>
{this.footerText}
</Footer>
</Layout>
<SubMenu
className="profile"
title={
<span className="submenu-title-wrapper">
<Icon type="user" />
{this.config.user}
</span>
}
>
<Logout />
</SubMenu>
</Menu>
</div>
);
}
</Header>
</Layout>
<Layout className="mobile-layout">
<div className="mobile-menu-button">
<Button type="link" onClick={this.showMobileNavigationBar}>
<Icon
type={this.state.collapsed ? 'menu-fold' : 'menu-unfold'}
className="bar-icon"
/>
</Button>
</div>
</Layout>
<Drawer
title={
<Link
to="/publisher/apps"
onClick={this.onCloseMobileNavigationBar}
>
<img
alt="logo"
src={this.Logo}
style={{ marginLeft: 30 }}
width={'60%'}
/>
</Link>
}
placement="left"
closable={false}
onClose={this.onCloseMobileNavigationBar}
visible={this.state.visible}
getContainer={false}
style={{ position: 'absolute' }}
>
<Menu
theme="light"
mode="inline"
defaultSelectedKeys={['1']}
style={{ lineHeight: '64px', width: 231 }}
onClick={this.onCloseMobileNavigationBar}
>
<Menu.Item key="1">
<Link to="/publisher/apps">
<Icon type="appstore" />
Apps
</Link>
</Menu.Item>
<SubMenu
title={
<span className="submenu-title-wrapper">
<Icon type="plus" />
Add New App
</span>
}
>
<Menu.Item key="setting:1">
<Link to="/publisher/add-new-app/public">Public APP</Link>
</Menu.Item>
<Menu.Item key="setting:2">
<Link to="/publisher/add-new-app/enterprise">
Enterprise APP
</Link>
</Menu.Item>
<Menu.Item key="setting:3">
<Link to="/publisher/add-new-app/web-clip">Web Clip</Link>
</Menu.Item>
<Menu.Item key="setting:4">
<Link to="/publisher/add-new-app/custom-app">Custom App</Link>
</Menu.Item>
</SubMenu>
<Menu.Item key="2">
<Link to="/publisher/manage">
<Icon type="control" />
Manage
</Link>
</Menu.Item>
</Menu>
</Drawer>
<Layout className="mobile-layout">
<Menu
mode="horizontal"
defaultSelectedKeys={['1']}
style={{ lineHeight: '63px', position: 'fixed', marginLeft: '80%' }}
>
<SubMenu
title={
<span className="submenu-title-wrapper">
<Icon type="user" />
</span>
}
>
<Logout />
</SubMenu>
</Menu>
</Layout>
<Layout className="dashboard-body">
<Content style={{ marginTop: 2 }}>
<Switch>
<Redirect exact from="/publisher" to="/publisher/apps" />
{this.state.routes.map(route => (
<RouteWithSubRoutes key={route.path} {...route} />
))}
</Switch>
</Content>
<Footer style={{ textAlign: 'center' }}>{this.footerText}</Footer>
</Layout>
</div>
);
}
}
export default withConfigContext(Dashboard);
export default withConfigContext(Dashboard);

@ -16,69 +16,65 @@
* under the License.
*/
import React from "react";
import {
PageHeader,
Typography,
Breadcrumb,
Icon
} from "antd";
import AddNewAppForm from "../../../components/new-app/AddNewAppForm";
import {Link} from "react-router-dom";
import React from 'react';
import { PageHeader, Typography, Breadcrumb, Icon } from 'antd';
import AddNewAppForm from '../../../components/new-app/AddNewAppForm';
import { Link } from 'react-router-dom';
const {Paragraph} = Typography;
const { Paragraph } = Typography;
const formConfig = {
installationType: "CUSTOM",
endpoint: "/custom-app",
jsonPayloadName: "application",
releaseWrapperName: "customAppReleaseWrappers",
specificElements: {
binaryFile: {
required: true
},
packageName : {
required: true
},
version : {
required: true
}
}
installationType: 'CUSTOM',
endpoint: '/custom-app',
jsonPayloadName: 'application',
releaseWrapperName: 'customAppReleaseWrappers',
specificElements: {
binaryFile: {
required: true,
},
packageName: {
required: true,
},
version: {
required: true,
},
},
};
class AddNewCustomApp extends React.Component {
constructor(props) {
super(props);
this.state = {
current: 0,
categories: [],
};
}
constructor(props) {
super(props);
this.state = {
current: 0,
categories: []
};
}
render() {
return (
<div>
<PageHeader style={{paddingTop:0, backgroundColor: "#fff"}}>
<Breadcrumb style={{paddingBottom:16}}>
<Breadcrumb.Item>
<Link to="/publisher/apps"><Icon type="home"/> Home</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Add New Custom App</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Add New Custom App</h3>
<Paragraph>Submit and share your own application to the corporate app store.</Paragraph>
</div>
</PageHeader>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 720}}>
<AddNewAppForm formConfig={formConfig}/>
</div>
</div>
);
}
render() {
return (
<div>
<PageHeader style={{ paddingTop: 0, backgroundColor: '#fff' }}>
<Breadcrumb style={{ paddingBottom: 16 }}>
<Breadcrumb.Item>
<Link to="/publisher/apps">
<Icon type="home" /> Home
</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Add New Custom App</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Add New Custom App</h3>
<Paragraph>
Submit and share your own application to the corporate app store.
</Paragraph>
</div>
</PageHeader>
<div style={{ background: '#f0f2f5', padding: 24, minHeight: 720 }}>
<AddNewAppForm formConfig={formConfig} />
</div>
</div>
);
}
}
export default AddNewCustomApp;

@ -16,63 +16,59 @@
* under the License.
*/
import React from "react";
import {
PageHeader,
Typography,
Breadcrumb,
Icon
} from "antd";
import AddNewAppForm from "../../../components/new-app/AddNewAppForm";
import {Link} from "react-router-dom";
import React from 'react';
import { PageHeader, Typography, Breadcrumb, Icon } from 'antd';
import AddNewAppForm from '../../../components/new-app/AddNewAppForm';
import { Link } from 'react-router-dom';
const {Paragraph} = Typography;
const { Paragraph } = Typography;
const formConfig = {
installationType: "ENTERPRISE",
endpoint: "/ent-app",
jsonPayloadName: "application",
releaseWrapperName: "entAppReleaseWrappers",
specificElements: {
binaryFile: {
required: true
}
}
installationType: 'ENTERPRISE',
endpoint: '/ent-app',
jsonPayloadName: 'application',
releaseWrapperName: 'entAppReleaseWrappers',
specificElements: {
binaryFile: {
required: true,
},
},
};
class AddNewEnterpriseApp extends React.Component {
constructor(props) {
super(props);
this.state = {
current: 0,
categories: [],
};
}
constructor(props) {
super(props);
this.state = {
current: 0,
categories: []
};
}
render() {
return (
<div>
<PageHeader style={{paddingTop:0, backgroundColor: "#fff"}}>
<Breadcrumb style={{paddingBottom:16}}>
<Breadcrumb.Item>
<Link to="/publisher/apps"><Icon type="home"/> Home</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Add New Enterprise App</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Add New Enterprise App</h3>
<Paragraph>Submit and share your own application to the corporate app store.</Paragraph>
</div>
</PageHeader>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 720}}>
<AddNewAppForm formConfig={formConfig}/>
</div>
</div>
);
}
render() {
return (
<div>
<PageHeader style={{ paddingTop: 0, backgroundColor: '#fff' }}>
<Breadcrumb style={{ paddingBottom: 16 }}>
<Breadcrumb.Item>
<Link to="/publisher/apps">
<Icon type="home" /> Home
</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Add New Enterprise App</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Add New Enterprise App</h3>
<Paragraph>
Submit and share your own application to the corporate app store.
</Paragraph>
</div>
</PageHeader>
<div style={{ background: '#f0f2f5', padding: 24, minHeight: 720 }}>
<AddNewAppForm formConfig={formConfig} />
</div>
</div>
);
}
}
export default AddNewEnterpriseApp;

@ -16,72 +16,67 @@
* under the License.
*/
import React from "react";
import {
Icon,
PageHeader,
Typography,
Breadcrumb
} from "antd";
import AddNewAppForm from "../../../components/new-app/AddNewAppForm";
import {Link} from "react-router-dom";
import React from 'react';
import { Icon, PageHeader, Typography, Breadcrumb } from 'antd';
import AddNewAppForm from '../../../components/new-app/AddNewAppForm';
import { Link } from 'react-router-dom';
const {Paragraph, Title} = Typography;
const { Paragraph } = Typography;
const formConfig = {
installationType: "PUBLIC",
endpoint: "/public-app",
jsonPayloadName:"public-app",
releaseWrapperName: "publicAppReleaseWrappers",
specificElements: {
packageName : {
required: true
},
version : {
required: true
}
}
installationType: 'PUBLIC',
endpoint: '/public-app',
jsonPayloadName: 'public-app',
releaseWrapperName: 'publicAppReleaseWrappers',
specificElements: {
packageName: {
required: true,
},
version: {
required: true,
},
},
};
class AddNewEnterpriseApp extends React.Component {
constructor(props) {
super(props);
this.state = {
current: 0,
categories: [],
};
}
constructor(props) {
super(props);
this.state = {
current: 0,
categories: []
};
}
componentDidMount() {
// this.getCategories();
}
componentDidMount() {
// this.getCategories();
}
render() {
return (
<div>
<PageHeader style={{paddingTop:0, backgroundColor: "#fff"}}>
<Breadcrumb style={{paddingBottom:16}}>
<Breadcrumb.Item>
<Link to="/publisher/apps"><Icon type="home"/> Home</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Add New Public App</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Add New Public App</h3>
<Paragraph>Share a public application in google play or apple store to your corporate app store.
</Paragraph>
</div>
</PageHeader>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 720}}>
<AddNewAppForm formConfig={formConfig}/>
</div>
</div>
);
}
render() {
return (
<div>
<PageHeader style={{ paddingTop: 0, backgroundColor: '#fff' }}>
<Breadcrumb style={{ paddingBottom: 16 }}>
<Breadcrumb.Item>
<Link to="/publisher/apps">
<Icon type="home" /> Home
</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Add New Public App</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Add New Public App</h3>
<Paragraph>
Share a public application in google play or apple store to your
corporate app store.
</Paragraph>
</div>
</PageHeader>
<div style={{ background: '#f0f2f5', padding: 24, minHeight: 720 }}>
<AddNewAppForm formConfig={formConfig} />
</div>
</div>
);
}
}
export default AddNewEnterpriseApp;

@ -16,67 +16,60 @@
* under the License.
*/
import React from "react";
import {
Icon,
PageHeader,
Typography,
Breadcrumb
} from "antd";
import AddNewAppForm from "../../../components/new-app/AddNewAppForm";
import {Link} from "react-router-dom";
import React from 'react';
import { Icon, PageHeader, Typography, Breadcrumb } from 'antd';
import AddNewAppForm from '../../../components/new-app/AddNewAppForm';
import { Link } from 'react-router-dom';
const {Paragraph, Title}= Typography;
const { Paragraph } = Typography;
const formConfig = {
installationType: "WEB_CLIP",
endpoint: "/web-app",
jsonPayloadName:"webapp",
releaseWrapperName: "webAppReleaseWrappers",
specificElements: {
url : {
required: true
},
version : {
required: true
}
}
installationType: 'WEB_CLIP',
endpoint: '/web-app',
jsonPayloadName: 'webapp',
releaseWrapperName: 'webAppReleaseWrappers',
specificElements: {
url: {
required: true,
},
version: {
required: true,
},
},
};
class AddNewEnterpriseApp extends React.Component {
constructor(props) {
super(props);
this.state = {
current: 0,
categories: [],
};
}
constructor(props) {
super(props);
this.state = {
current: 0,
categories: []
};
}
render() {
return (
<div>
<PageHeader style={{paddingTop:0, backgroundColor: "#fff"}}>
<Breadcrumb style={{paddingBottom:16}}>
<Breadcrumb.Item>
<Link to="/publisher/apps"><Icon type="home"/> Home</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Add New Web Clip</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Add New Web Clip</h3>
<Paragraph>Share a Web Clip to your corporate app store.</Paragraph>
</div>
</PageHeader>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 720}}>
<AddNewAppForm formConfig={formConfig}/>
</div>
</div>
);
}
render() {
return (
<div>
<PageHeader style={{ paddingTop: 0, backgroundColor: '#fff' }}>
<Breadcrumb style={{ paddingBottom: 16 }}>
<Breadcrumb.Item>
<Link to="/publisher/apps">
<Icon type="home" /> Home
</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Add New Web Clip</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Add New Web Clip</h3>
<Paragraph>Share a Web Clip to your corporate app store.</Paragraph>
</div>
</PageHeader>
<div style={{ background: '#f0f2f5', padding: 24, minHeight: 720 }}>
<AddNewAppForm formConfig={formConfig} />
</div>
</div>
);
}
}
export default AddNewEnterpriseApp;

@ -16,52 +16,46 @@
* under the License.
*/
import React from "react";
import {
Icon,
PageHeader,
Typography,
Breadcrumb
} from "antd";
import AddNewReleaseForm from "../../../components/new-release/AddReleaseForm";
import {Link} from "react-router-dom";
import React from 'react';
import { Icon, PageHeader, Typography, Breadcrumb } from 'antd';
import AddNewReleaseForm from '../../../components/new-release/AddReleaseForm';
import { Link } from 'react-router-dom';
const Paragraph = Typography;
class AddNewRelease extends React.Component {
constructor(props) {
super(props);
this.state = {
current: 0,
categories: []
};
}
render() {
const {appId, deviceType} = this.props.match.params;
return (
<div>
<PageHeader style={{paddingTop:0, backgroundColor: "#fff"}}>
<Breadcrumb style={{paddingBottom: 16}}>
<Breadcrumb.Item>
<Link to="/publisher/apps"><Icon type="home"/> Home</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Add New Release</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Add New Release</h3>
<Paragraph>Add new release for the application</Paragraph>
</div>
</PageHeader>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 720}}>
<AddNewReleaseForm deviceType={deviceType} appId={appId} />
</div>
</div>
);
}
constructor(props) {
super(props);
this.state = {
current: 0,
categories: [],
};
}
render() {
const { appId, deviceType } = this.props.match.params;
return (
<div>
<PageHeader style={{ paddingTop: 0, backgroundColor: '#fff' }}>
<Breadcrumb style={{ paddingBottom: 16 }}>
<Breadcrumb.Item>
<Link to="/publisher/apps">
<Icon type="home" /> Home
</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Add New Release</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Add New Release</h3>
<Paragraph>Add new release for the application</Paragraph>
</div>
</PageHeader>
<div style={{ background: '#f0f2f5', padding: 24, minHeight: 720 }}>
<AddNewReleaseForm deviceType={deviceType} appId={appId} />
</div>
</div>
);
}
}
export default AddNewRelease;

@ -16,28 +16,25 @@
* under the License.
*/
import React from "react";
import ListApps from "../../../components/apps/list-apps/ListApps";
import React from 'react';
import ListApps from '../../../components/apps/list-apps/ListApps';
class Apps extends React.Component {
routes;
constructor(props) {
super(props);
this.routes = props.routes;
routes;
constructor(props) {
super(props);
this.routes = props.routes;
}
}
render() {
return (
<div>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 780}}>
<ListApps/>
</div>
</div>
);
}
render() {
return (
<div>
<div style={{ background: '#f0f2f5', padding: 24, minHeight: 780 }}>
<ListApps />
</div>
</div>
);
}
}
export default Apps;

@ -16,193 +16,232 @@
* under the License.
*/
import React from "react";
import React from 'react';
import '../../../../App.css';
import {Typography, Row, Col, message, Card, notification, Skeleton} from "antd";
import { Typography, Row, Col, Card, Skeleton } from 'antd';
import axios from 'axios';
import ReleaseView from "../../../../components/apps/release/ReleaseView";
import LifeCycle from "../../../../components/apps/release/lifeCycle/LifeCycle";
import {withConfigContext} from "../../../../context/ConfigContext";
import {handleApiError} from "../../../../js/Utils";
import NewAppUploadForm from "../../../../components/new-app/subForms/NewAppUploadForm";
import ReleaseView from '../../../../components/apps/release/ReleaseView';
import LifeCycle from '../../../../components/apps/release/lifeCycle/LifeCycle';
import { withConfigContext } from '../../../../context/ConfigContext';
import { handleApiError } from '../../../../js/Utils';
const {Title} = Typography;
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,
release: null,
currentLifecycleStatus: null,
lifecycle: null,
supportedOsVersions: [],
forbiddenErrors: {
supportedOsVersions: false,
lifeCycle: false
}
};
}
componentDidMount() {
const {uuid} = this.props.match.params;
this.fetchData(uuid);
this.getLifecycle();
}
changeCurrentLifecycleStatus = (status) => {
this.setState({
currentLifecycleStatus: status
});
};
updateRelease = (release) => {
this.setState({
release
});
};
fetchData = (uuid) => {
const config = this.props.context;
//send request to the invoker
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications/release/" + uuid,
).then(res => {
if (res.status === 200) {
const app = res.data.data;
const release = (app !== null) ? app.applicationReleases[0] : null;
const currentLifecycleStatus = (release !== null) ? release.currentStatus : null;
this.setState({
app: app,
release: release,
currentLifecycleStatus: currentLifecycleStatus,
loading: false,
uuid: uuid
});
if (config.deviceTypes.mobileTypes.includes(app.deviceType)) {
this.getSupportedOsVersions(app.deviceType);
}
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load the release.");
this.setState({loading: false});
});
};
getLifecycle = () => {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications/lifecycle-config"
).then(res => {
if (res.status === 200) {
const lifecycle = res.data.data;
this.setState({
lifecycle: lifecycle
})
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load lifecycle configuration.", true);
if (error.hasOwnProperty("response") && error.response.status === 403) {
const {forbiddenErrors} = this.state;
forbiddenErrors.lifeCycle = true;
this.setState({
forbiddenErrors
})
}
});
};
getSupportedOsVersions = (deviceType) => {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt +
`/admin/device-types/${deviceType}/versions`
).then(res => {
if (res.status === 200) {
let supportedOsVersions = JSON.parse(res.data.data);
this.setState({
supportedOsVersions
});
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load supported OS versions.", true);
if (error.hasOwnProperty("response") && error.response.status === 403) {
const {forbiddenErrors} = this.state;
forbiddenErrors.supportedOsVersions = true;
this.setState({
forbiddenErrors,
loading: false
})
} else {
this.setState({
loading: false
});
}
});
routes;
constructor(props) {
super(props);
this.routes = props.routes;
this.state = {
loading: true,
app: null,
uuid: null,
release: null,
currentLifecycleStatus: null,
lifecycle: null,
supportedOsVersions: [],
forbiddenErrors: {
supportedOsVersions: false,
lifeCycle: false,
},
};
render() {
const {app, release, currentLifecycleStatus, lifecycle, loading, forbiddenErrors} = this.state;
if (release == null && loading === false) {
return (
<div style={{background: '#f0f2f5', padding: 24, minHeight: 780}}>
<Title level={3}>No Apps Found</Title>
</div>
);
}
componentDidMount() {
const { uuid } = this.props.match.params;
this.fetchData(uuid);
this.getLifecycle();
}
changeCurrentLifecycleStatus = status => {
this.setState({
currentLifecycleStatus: status,
});
};
updateRelease = release => {
this.setState({
release,
});
};
fetchData = uuid => {
const config = this.props.context;
// send request to the invoker
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/applications/release/' +
uuid,
)
.then(res => {
if (res.status === 200) {
const app = res.data.data;
const release = app !== null ? app.applicationReleases[0] : null;
const currentLifecycleStatus =
release !== null ? release.currentStatus : null;
this.setState({
app: app,
release: release,
currentLifecycleStatus: currentLifecycleStatus,
loading: false,
uuid: uuid,
});
if (config.deviceTypes.mobileTypes.includes(app.deviceType)) {
this.getSupportedOsVersions(app.deviceType);
}
}
//todo remove uppercase
return (
<div>
<div className="main-container">
<Row style={{padding: 10}}>
<Col lg={16} md={24} style={{padding: 3}}>
<Card>
<Skeleton loading={loading} avatar={{size: 'large'}} active paragraph={{rows: 18}}>
{(release !== null) && (
<ReleaseView
forbiddenErrors={forbiddenErrors}
app={app}
release={release}
currentLifecycleStatus={currentLifecycleStatus}
lifecycle={lifecycle}
updateRelease={this.updateRelease}
supportedOsVersions={[...this.state.supportedOsVersions]}
/>)
}
</Skeleton>
</Card>
</Col>
<Col lg={8} md={24} style={{padding: 3}}>
<Card lg={8} md={24}>
<Skeleton loading={loading} active paragraph={{rows: 8}}>
{(release !== null) && (
<LifeCycle
uuid={release.uuid}
currentStatus={release.currentStatus.toUpperCase()}
changeCurrentLifecycleStatus={this.changeCurrentLifecycleStatus}
lifecycle={lifecycle}
/>)
}
</Skeleton>
</Card>
</Col>
</Row>
</div>
</div>
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load the release.',
);
this.setState({ loading: false });
});
};
getLifecycle = () => {
const config = this.props.context;
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/applications/lifecycle-config',
)
.then(res => {
if (res.status === 200) {
const lifecycle = res.data.data;
this.setState({
lifecycle: lifecycle,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load lifecycle configuration.',
true,
);
if (error.hasOwnProperty('response') && error.response.status === 403) {
const { forbiddenErrors } = this.state;
forbiddenErrors.lifeCycle = true;
this.setState({
forbiddenErrors,
});
}
});
};
getSupportedOsVersions = deviceType => {
const config = this.props.context;
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.deviceMgt +
`/admin/device-types/${deviceType}/versions`,
)
.then(res => {
if (res.status === 200) {
let supportedOsVersions = JSON.parse(res.data.data);
this.setState({
supportedOsVersions,
});
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to load supported OS versions.',
true,
);
if (error.hasOwnProperty('response') && error.response.status === 403) {
const { forbiddenErrors } = this.state;
forbiddenErrors.supportedOsVersions = true;
this.setState({
forbiddenErrors,
loading: false,
});
} else {
this.setState({
loading: false,
});
}
});
};
render() {
const {
app,
release,
currentLifecycleStatus,
lifecycle,
loading,
forbiddenErrors,
} = this.state;
if (release == null && loading === false) {
return (
<div style={{ background: '#f0f2f5', padding: 24, minHeight: 780 }}>
<Title level={3}>No Apps Found</Title>
</div>
);
}
// todo remove uppercase
return (
<div>
<div className="main-container">
<Row style={{ padding: 10 }}>
<Col lg={16} md={24} style={{ padding: 3 }}>
<Card>
<Skeleton
loading={loading}
avatar={{ size: 'large' }}
active
paragraph={{ rows: 18 }}
>
{release !== null && (
<ReleaseView
forbiddenErrors={forbiddenErrors}
app={app}
release={release}
currentLifecycleStatus={currentLifecycleStatus}
lifecycle={lifecycle}
updateRelease={this.updateRelease}
supportedOsVersions={[...this.state.supportedOsVersions]}
/>
)}
</Skeleton>
</Card>
</Col>
<Col lg={8} md={24} style={{ padding: 3 }}>
<Card lg={8} md={24}>
<Skeleton loading={loading} active paragraph={{ rows: 8 }}>
{release !== null && (
<LifeCycle
uuid={release.uuid}
currentStatus={release.currentStatus.toUpperCase()}
changeCurrentLifecycleStatus={
this.changeCurrentLifecycleStatus
}
lifecycle={lifecycle}
/>
)}
</Skeleton>
</Card>
</Col>
</Row>
</div>
</div>
);
}
}
export default withConfigContext(Release);

@ -16,64 +16,66 @@
* under the License.
*/
import React from "react";
import {notification, Menu, Icon} from 'antd';
import React from 'react';
import { notification, Menu, Icon } from 'antd';
import axios from 'axios';
import {withConfigContext} from "../../../context/ConfigContext";
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
};
}
/*
constructor(props) {
super(props);
this.state = {
inValid: false,
loading: false,
};
}
/*
This function call the logout api when the request is success
*/
handleSubmit = () => {
handleSubmit = () => {
const thisForm = this;
const config = this.props.context;
const thisForm = this;
const config = this.props.context;
thisForm.setState({
inValid: false,
});
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 + "/publisher/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.",
});
}
});
};
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 + '/publisher/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>
);
}
render() {
return (
<Menu>
<Menu.Item key="1" onClick={this.handleSubmit}>
<Icon type="logout" />
Logout
</Menu.Item>
</Menu>
);
}
}
export default withConfigContext(Logout);

@ -16,56 +16,55 @@
* under the License.
*/
import React from "react";
import {PageHeader, Typography, Breadcrumb, Row, Col, Icon} from "antd";
import ManageCategories from "../../../components/manage/categories/ManageCategories";
import ManageTags from "../../../components/manage/categories/ManageTags";
import {Link} from "react-router-dom";
import React from 'react';
import { PageHeader, Typography, Breadcrumb, Row, Col, Icon } from 'antd';
import ManageCategories from '../../../components/manage/categories/ManageCategories';
import ManageTags from '../../../components/manage/categories/ManageTags';
import { Link } from 'react-router-dom';
const {Paragraph} = Typography;
const { Paragraph } = Typography;
class Manage extends React.Component {
routes;
routes;
constructor(props) {
super(props);
this.routes = props.routes;
constructor(props) {
super(props);
this.routes = props.routes;
}
}
render() {
return (
<div>
<PageHeader style={{paddingTop:0, backgroundColor: "#fff"}}>
<Breadcrumb style={{paddingBottom: 16}}>
<Breadcrumb.Item>
<Link to="/publisher/apps"><Icon type="home"/> Home</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>
Manage
</Breadcrumb.Item>
<Breadcrumb.Item>General</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Manage General Settings</h3>
<Paragraph>Maintain and manage categories and tags here..</Paragraph>
</div>
</PageHeader>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 780}}>
<Row gutter={16}>
<Col sm={24} md={12}>
<ManageCategories/>
</Col>
<Col sm={24} md={12}>
<ManageTags/>
</Col>
</Row>
</div>
</div>
);
}
render() {
return (
<div>
<PageHeader style={{ paddingTop: 0, backgroundColor: '#fff' }}>
<Breadcrumb style={{ paddingBottom: 16 }}>
<Breadcrumb.Item>
<Link to="/publisher/apps">
<Icon type="home" /> Home
</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Manage</Breadcrumb.Item>
<Breadcrumb.Item>General</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Manage General Settings</h3>
<Paragraph>
Maintain and manage categories and tags here..
</Paragraph>
</div>
</PageHeader>
<div style={{ background: '#f0f2f5', padding: 24, minHeight: 780 }}>
<Row gutter={16}>
<Col sm={24} md={12}>
<ManageCategories />
</Col>
<Col sm={24} md={12}>
<ManageTags />
</Col>
</Row>
</div>
</div>
);
}
}
export default Manage;

@ -16,53 +16,50 @@
* under the License.
*/
import React from "react";
import {PageHeader, Typography, Breadcrumb, Divider, Button, Icon} from "antd";
import {Link} from "react-router-dom";
import SyncAndroidApps from "../../../../components/manage/android-enterprise/SyncAndroidApps";
import {withConfigContext} from "../../../../context/ConfigContext";
import GooglePlayIframe from "../../../../components/manage/android-enterprise/GooglePlayIframe";
import Pages from "../../../../components/manage/android-enterprise/Pages/Pages";
const {Paragraph} = Typography;
import React from 'react';
import { PageHeader, Breadcrumb, Divider, Icon } from 'antd';
import { Link } from 'react-router-dom';
import SyncAndroidApps from '../../../../components/manage/android-enterprise/SyncAndroidApps';
import { withConfigContext } from '../../../../context/ConfigContext';
import GooglePlayIframe from '../../../../components/manage/android-enterprise/GooglePlayIframe';
import Pages from '../../../../components/manage/android-enterprise/Pages/Pages';
class ManageAndroidEnterprise extends React.Component {
routes;
constructor(props) {
super(props);
this.routes = props.routes;
this.config = this.props.context;
}
routes;
render() {
return (
<div>
<PageHeader style={{paddingTop:0, backgroundColor: "#fff"}}>
<Breadcrumb style={{paddingBottom: 16}}>
<Breadcrumb.Item>
<Link to="/publisher/apps"><Icon type="home"/> Home</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>
Manage
</Breadcrumb.Item>
<Breadcrumb.Item>Android Enterprise</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Manage Android Enterprise</h3>
{/*<Paragraph>Lorem ipsum</Paragraph>*/}
</div>
</PageHeader>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 720}}>
<SyncAndroidApps/>
<GooglePlayIframe/>
<Divider/>
<Pages/>
</div>
</div>
constructor(props) {
super(props);
this.routes = props.routes;
this.config = this.props.context;
}
);
}
render() {
return (
<div>
<PageHeader style={{ paddingTop: 0, backgroundColor: '#fff' }}>
<Breadcrumb style={{ paddingBottom: 16 }}>
<Breadcrumb.Item>
<Link to="/publisher/apps">
<Icon type="home" /> Home
</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Manage</Breadcrumb.Item>
<Breadcrumb.Item>Android Enterprise</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Manage Android Enterprise</h3>
{/* <Paragraph>Lorem ipsum</Paragraph>*/}
</div>
</PageHeader>
<div style={{ background: '#f0f2f5', padding: 24, minHeight: 720 }}>
<SyncAndroidApps />
<GooglePlayIframe />
<Divider />
<Pages />
</div>
</div>
);
}
}
export default withConfigContext(ManageAndroidEnterprise);

@ -16,371 +16,409 @@
* under the License.
*/
import React from "react";
import React from 'react';
import {
PageHeader,
Typography,
Breadcrumb,
Button,
Icon,
Col,
Row,
notification,
message,
Spin,
Select,
Tag,
Divider
} from "antd";
import {Link, withRouter} from "react-router-dom";
import {withConfigContext} from "../../../../../context/ConfigContext";
import axios from "axios";
import Cluster from "../../../../../components/manage/android-enterprise/Pages/Cluster/Cluster";
import EditLinks from "../../../../../components/manage/android-enterprise/Pages/EditLinks/EditLinks";
import {handleApiError} from "../../../../../js/Utils";
const {Option} = Select;
const {Title, Text} = Typography;
PageHeader,
Typography,
Breadcrumb,
Button,
Icon,
Col,
Row,
notification,
message,
Spin,
Tag,
Divider,
} from 'antd';
import { Link, withRouter } from 'react-router-dom';
import { withConfigContext } from '../../../../../context/ConfigContext';
import axios from 'axios';
import Cluster from '../../../../../components/manage/android-enterprise/Pages/Cluster/Cluster';
import EditLinks from '../../../../../components/manage/android-enterprise/Pages/EditLinks/EditLinks';
import { handleApiError } from '../../../../../js/Utils';
const { Title } = Typography;
class Page extends React.Component {
routes;
constructor(props) {
super(props);
const {pageName, pageId} = this.props.match.params;
this.pageId = pageId;
this.routes = props.routes;
this.config = this.props.context;
this.pages = [];
this.pageNames = {};
this.state = {
pageName,
clusters: [],
loading: false,
applications: [],
isAddNewClusterVisible: false,
links: []
};
}
componentDidMount() {
this.fetchClusters();
this.fetchApplications();
this.fetchPages();
}
removeLoadedCluster = (clusterId) => {
const clusters = [...this.state.clusters];
let index = -1;
for (let i = 0; i < clusters.length; i++) {
if (clusters[i].clusterId === clusterId) {
index = i;
break;
}
}
clusters.splice(index, 1);
this.setState({
clusters
});
routes;
constructor(props) {
super(props);
const { pageName, pageId } = this.props.match.params;
this.pageId = pageId;
this.routes = props.routes;
this.config = this.props.context;
this.pages = [];
this.pageNames = {};
this.state = {
pageName,
clusters: [],
loading: false,
applications: [],
isAddNewClusterVisible: false,
links: [],
};
updatePageName = pageName => {
const config = this.props.context;
if (pageName !== this.state.pageName && pageName !== "") {
const data = {
locale: "en",
pageName: pageName,
pageId: this.pageId
};
axios.put(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/page",
data
).then(res => {
if (res.status === 200) {
notification["success"]({
message: 'Saved!',
description: 'Page name updated successfully!'
});
this.setState({
loading: false,
pageName: res.data.data.pageName,
});
this.props.history.push(`/publisher/manage/android-enterprise/pages/${pageName}/${this.pageId}`);
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to save the page name.");
this.setState({loading: false});
}
componentDidMount() {
this.fetchClusters();
this.fetchApplications();
this.fetchPages();
}
removeLoadedCluster = clusterId => {
const clusters = [...this.state.clusters];
let index = -1;
for (let i = 0; i < clusters.length; i++) {
if (clusters[i].clusterId === clusterId) {
index = i;
break;
}
}
clusters.splice(index, 1);
this.setState({
clusters,
});
};
updatePageName = pageName => {
const config = this.props.context;
if (pageName !== this.state.pageName && pageName !== '') {
const data = {
locale: 'en',
pageName: pageName,
pageId: this.pageId,
};
axios
.put(
window.location.origin +
config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/store-layout/page',
data,
)
.then(res => {
if (res.status === 200) {
notification.success({
message: 'Saved!',
description: 'Page name updated successfully!',
});
}
};
swapClusters = (index, swapIndex) => {
const clusters = [...this.state.clusters];
if (swapIndex !== -1 && index < clusters.length) {
// swap elements
[clusters[index], clusters[swapIndex]] = [clusters[swapIndex], clusters[index]];
this.setState({
clusters,
loading: false,
pageName: res.data.data.pageName,
});
}
};
fetchPages = () => {
const config = this.props.context;
this.setState({loading: true});
//send request to the invoker
axios.get(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/page",
).then(res => {
if (res.status === 200) {
this.pages = res.data.data.page;
let links = [];
this.pages.forEach((page) => {
this.pageNames[page.id.toString()] = page.name[0]["text"];
if (page.id === this.pageId && page.hasOwnProperty("link")) {
links = page["link"];
}
});
this.setState({
loading: false,
links
});
}
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin + '/publisher/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load pages.",
});
}
this.setState({loading: false});
this.props.history.push(
`/publisher/manage/android-enterprise/pages/${pageName}/${this.pageId}`,
);
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to save the page name.',
);
this.setState({ loading: false });
});
};
}
};
fetchClusters = () => {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri +
`/device-mgt/android/v1.0/enterprise/store-layout/page/${this.pageId}/clusters`
).then(res => {
if (res.status === 200) {
let clusters = JSON.parse(res.data.data);
// sort according to the orderInPage value
clusters.sort((a, b) => (a.orderInPage > b.orderInPage) ? 1 : -1);
this.setState({
clusters,
loading: false
});
}
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
window.location.href = window.location.origin + '/publisher/login';
} else if (!(error.hasOwnProperty("response") && error.response.status === 404)) {
// API sends 404 when no apps
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load clusters.",
});
}
this.setState({
loading: false
});
});
};
swapClusters = (index, swapIndex) => {
const clusters = [...this.state.clusters];
//fetch applications
fetchApplications = () => {
const config = this.props.context;
this.setState({loading: true});
const filters = {
appType: "PUBLIC",
deviceType: "android"
};
//send request to the invoker
axios.post(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications",
filters
).then(res => {
if (res.status === 200) {
const applications = res.data.data.applications.map(application => {
const release = application.applicationReleases[0];
return {
packageId: `app:${application.packageName}`,
iconUrl: release.iconPath,
name: application.name
}
});
this.setState({
loading: false,
applications,
});
}
if (swapIndex !== -1 && index < clusters.length) {
// swap elements
[clusters[index], clusters[swapIndex]] = [
clusters[swapIndex],
clusters[index],
];
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin + '/publisher/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load pages.",
});
this.setState({
clusters,
});
}
};
fetchPages = () => {
const config = this.props.context;
this.setState({ loading: true });
// send request to the invoker
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/store-layout/page',
)
.then(res => {
if (res.status === 200) {
this.pages = res.data.data.page;
let links = [];
this.pages.forEach(page => {
this.pageNames[page.id.toString()] = page.name[0].text;
if (page.id === this.pageId && page.hasOwnProperty('link')) {
links = page.link;
}
});
this.setState({loading: false});
});
};
this.setState({
loading: false,
links,
});
}
})
.catch(error => {
if (error.hasOwnProperty('response') && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin + '/publisher/login';
} else {
notification.error({
message: 'There was a problem',
duration: 0,
description: 'Error occurred while trying to load pages.',
});
}
toggleAddNewClusterVisibility = (isAddNewClusterVisible) => {
this.setState({ loading: false });
});
};
fetchClusters = () => {
const config = this.props.context;
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
`/device-mgt/android/v1.0/enterprise/store-layout/page/${this.pageId}/clusters`,
)
.then(res => {
if (res.status === 200) {
let clusters = JSON.parse(res.data.data);
// sort according to the orderInPage value
clusters.sort((a, b) => (a.orderInPage > b.orderInPage ? 1 : -1));
this.setState({
clusters,
loading: false,
});
}
})
.catch(error => {
if (error.hasOwnProperty('response') && error.response.status === 401) {
window.location.href = window.location.origin + '/publisher/login';
} else if (
!(error.hasOwnProperty('response') && error.response.status === 404)
) {
// API sends 404 when no apps
notification.error({
message: 'There was a problem',
duration: 0,
description: 'Error occurred while trying to load clusters.',
});
}
this.setState({
isAddNewClusterVisible
loading: false,
});
};
});
};
addSavedClusterToThePage = (cluster) => {
this.setState({
clusters: [...this.state.clusters, cluster],
isAddNewClusterVisible: false
});
window.scrollTo(0, document.body.scrollHeight);
};
// fetch applications
fetchApplications = () => {
const config = this.props.context;
this.setState({ loading: true });
updateLinks = (links) =>{
this.setState({
links
});
const filters = {
appType: 'PUBLIC',
deviceType: 'android',
};
render() {
const {pageName, loading, clusters, applications, isAddNewClusterVisible, links} = this.state;
return (
<div>
<PageHeader style={{paddingTop:0, backgroundColor: "#fff"}}>
<Breadcrumb style={{paddingBottom: 16}}>
<Breadcrumb.Item>
<Link to="/publisher/apps"><Icon type="home"/> Home</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>
Manage
</Breadcrumb.Item>
<Breadcrumb.Item>
<Link to="/publisher/manage/android-enterprise">Android Enterprise</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Manage Page</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Manage Android Enterprise</h3>
{/*<Paragraph>Lorem ipsum</Paragraph>*/}
</div>
</PageHeader>
<Spin spinning={loading}>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 720}}>
<Row>
<Col md={8} sm={18} xs={24}>
<Title editable={{onChange: this.updatePageName}} level={2}>{pageName}</Title>
</Col>
</Row>
<Row>
<Col>
<Title level={4}>Links</Title>
{
links.map(link => {
if (this.pageNames.hasOwnProperty(link.toString())) {
return <Tag key={link}
color="#87d068">{this.pageNames[link.toString()]}</Tag>
} else {
return null;
}
})
}
<EditLinks
updateLinks={this.updateLinks}
pageId={this.pageId}
selectedLinks={links}
pages={this.pages}/>
</Col>
{/*<Col>*/}
{/*</Col>*/}
</Row>
<Divider dashed={true}/>
<Title level={4}>Clusters</Title>
<div hidden={isAddNewClusterVisible} style={{textAlign: "center"}}>
<Button
type="dashed"
shape="round"
icon="plus"
size="large"
onClick={() => {
this.toggleAddNewClusterVisibility(true);
}}
>Add new cluster</Button>
</div>
<div hidden={!isAddNewClusterVisible}>
<Cluster
cluster={{
clusterId: 0,
name: "New Cluster",
products: []
}}
orderInPage={clusters.length}
isTemporary={true}
pageId={this.pageId}
applications={applications}
addSavedClusterToThePage={this.addSavedClusterToThePage}
toggleAddNewClusterVisibility={this.toggleAddNewClusterVisibility}/>
</div>
{
clusters.map((cluster, index) => {
return (
<Cluster
key={cluster.clusterId}
index={index}
orderInPage={cluster.orderInPage}
isTemporary={false}
cluster={cluster}
pageId={this.pageId}
applications={applications}
swapClusters={this.swapClusters}
removeLoadedCluster={this.removeLoadedCluster}/>
);
})
}
</div>
</Spin>
// send request to the invoker
axios
.post(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.publisher +
'/applications',
filters,
)
.then(res => {
if (res.status === 200) {
const applications = res.data.data.applications.map(application => {
const release = application.applicationReleases[0];
return {
packageId: `app:${application.packageName}`,
iconUrl: release.iconPath,
name: application.name,
};
});
this.setState({
loading: false,
applications,
});
}
})
.catch(error => {
if (error.hasOwnProperty('response') && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin + '/publisher/login';
} else {
notification.error({
message: 'There was a problem',
duration: 0,
description: 'Error occurred while trying to load pages.',
});
}
this.setState({ loading: false });
});
};
toggleAddNewClusterVisibility = isAddNewClusterVisible => {
this.setState({
isAddNewClusterVisible,
});
};
addSavedClusterToThePage = cluster => {
this.setState({
clusters: [...this.state.clusters, cluster],
isAddNewClusterVisible: false,
});
window.scrollTo(0, document.body.scrollHeight);
};
updateLinks = links => {
this.setState({
links,
});
};
render() {
const {
pageName,
loading,
clusters,
applications,
isAddNewClusterVisible,
links,
} = this.state;
return (
<div>
<PageHeader style={{ paddingTop: 0, backgroundColor: '#fff' }}>
<Breadcrumb style={{ paddingBottom: 16 }}>
<Breadcrumb.Item>
<Link to="/publisher/apps">
<Icon type="home" /> Home
</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Manage</Breadcrumb.Item>
<Breadcrumb.Item>
<Link to="/publisher/manage/android-enterprise">
Android Enterprise
</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Manage Page</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Manage Android Enterprise</h3>
{/* <Paragraph>Lorem ipsum</Paragraph>*/}
</div>
</PageHeader>
<Spin spinning={loading}>
<div style={{ background: '#f0f2f5', padding: 24, minHeight: 720 }}>
<Row>
<Col md={8} sm={18} xs={24}>
<Title editable={{ onChange: this.updatePageName }} level={2}>
{pageName}
</Title>
</Col>
</Row>
<Row>
<Col>
<Title level={4}>Links</Title>
{links.map(link => {
if (this.pageNames.hasOwnProperty(link.toString())) {
return (
<Tag key={link} color="#87d068">
{this.pageNames[link.toString()]}
</Tag>
);
}
return null;
})}
<EditLinks
updateLinks={this.updateLinks}
pageId={this.pageId}
selectedLinks={links}
pages={this.pages}
/>
</Col>
{/* <Col>*/}
{/* </Col>*/}
</Row>
<Divider dashed={true} />
<Title level={4}>Clusters</Title>
<div
hidden={isAddNewClusterVisible}
style={{ textAlign: 'center' }}
>
<Button
type="dashed"
shape="round"
icon="plus"
size="large"
onClick={() => {
this.toggleAddNewClusterVisibility(true);
}}
>
Add new cluster
</Button>
</div>
<div hidden={!isAddNewClusterVisible}>
<Cluster
cluster={{
clusterId: 0,
name: 'New Cluster',
products: [],
}}
orderInPage={clusters.length}
isTemporary={true}
pageId={this.pageId}
applications={applications}
addSavedClusterToThePage={this.addSavedClusterToThePage}
toggleAddNewClusterVisibility={
this.toggleAddNewClusterVisibility
}
/>
</div>
);
}
{clusters.map((cluster, index) => {
return (
<Cluster
key={cluster.clusterId}
index={index}
orderInPage={cluster.orderInPage}
isTemporary={false}
cluster={cluster}
pageId={this.pageId}
applications={applications}
swapClusters={this.swapClusters}
removeLoadedCluster={this.removeLoadedCluster}
/>
);
})}
</div>
</Spin>
</div>
);
}
}
export default withConfigContext(withRouter(Page));

@ -34,8 +34,8 @@ const isLocalhost = Boolean(
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}$/
)
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
),
);
export function register(config) {
@ -61,7 +61,7 @@ export function register(config) {
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'
'worker. To learn more, visit https://bit.ly/CRA-PWA',
);
});
} else {
@ -89,7 +89,7 @@ function registerValidSW(swUrl, config) {
// 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.'
'tabs for this page are closed. See https://bit.ly/CRA-PWA.',
);
// Execute callback
@ -139,7 +139,7 @@ function checkValidServiceWorker(swUrl, config) {
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
'No internet connection found. App is running in offline mode.',
);
});
}

@ -17,119 +17,119 @@
*/
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 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: '/publisher/'
devtool: 'source-map',
output: {
publicPath: '/publisher/',
},
watch: false,
resolve: {
alias: {
AppData: path.resolve(__dirname, 'source/src/app/common/'),
AppComponents: path.resolve(__dirname, 'source/src/app/components/'),
},
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"
]
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: /\.scss$/,
use: ['style-loader', 'scss-loader']
},
],
},
{
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,
},
{
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"
})
},
{
loader: 'img-loader',
},
],
},
],
externals: {
'Config': JSON.stringify(require('./public/conf/config.json'))
}
},
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;
if (process.env.NODE_ENV === 'development') {
config.watch = true;
}
module.exports = config;

Loading…
Cancel
Save