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
This commit is contained in:
Dharmakeerthi Lasantha 2020-01-16 07:15:52 +00:00
commit 751f2b4bb8
50 changed files with 8713 additions and 7519 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -56,6 +56,12 @@
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"chai": "^4.1.2", "chai": "^4.1.2",
"css-loader": "^0.28.11", "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": "^4.17.1",
"express-pino-logger": "^4.0.0", "express-pino-logger": "^4.0.0",
"file-loader": "^2.0.0", "file-loader": "^2.0.0",
@ -74,6 +80,7 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"pino-colada": "^1.4.5", "pino-colada": "^1.4.5",
"postcss-loader": "^3.0.0", "postcss-loader": "^3.0.0",
"prettier": "1.18.1",
"react": "^16.8.6", "react": "^16.8.6",
"react-dom": "^16.8.6", "react-dom": "^16.8.6",
"react-intl": "^2.9.0", "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_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 ", "build_dev": "NODE_ENV=development webpack -d --watch ",
"server": "node-env-run server --exec nodemon | pino-colada", "server": "node-env-run server --exec nodemon | pino-colada",
"dev2": "run-p server start" "dev2": "run-p server start",
"lint": "eslint \"src/**/*.js\""
} }
} }

View File

@ -16,155 +16,168 @@
* under the License. * under the License.
*/ */
import React from "react"; import React from 'react';
import "antd/dist/antd.less"; import 'antd/dist/antd.less';
import RouteWithSubRoutes from "./components/RouteWithSubRoutes"; import RouteWithSubRoutes from './components/RouteWithSubRoutes';
import { import { BrowserRouter as Router, Redirect, Switch } from 'react-router-dom';
BrowserRouter as Router, import axios from 'axios';
Redirect, Switch, import { Layout, Spin, Result } from 'antd';
} from 'react-router-dom'; import ConfigContext from './context/ConfigContext';
import axios from "axios";
import {Layout, Spin, Result, notification} from "antd";
import ConfigContext from "./context/ConfigContext";
const {Content} = Layout; const { Content } = Layout;
const loadingView = ( const loadingView = (
<Layout> <Layout>
<Content style={{ <Content
padding: '0 0', style={{
paddingTop: 300, padding: '0 0',
backgroundColor: '#fff', paddingTop: 300,
textAlign: 'center' backgroundColor: '#fff',
}}> textAlign: 'center',
<Spin tip="Loading..."/> }}
</Content> >
</Layout> <Spin tip="Loading..." />
</Content>
</Layout>
); );
const errorView = ( const errorView = (
<Result <Result
style={{ style={{
paddingTop: 200 paddingTop: 200,
}} }}
status="500" status="500"
title="Error occurred while loading the configuration" title="Error occurred while loading the configuration"
subTitle="Please refresh your browser window" subTitle="Please refresh your browser window"
/> />
); );
class App extends React.Component { class App extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
error: false,
config: {},
};
}
constructor(props) { componentDidMount() {
super(props); this.updateFavicon();
this.state = { axios
loading: true, .get(window.location.origin + '/publisher/public/conf/config.json')
error: false, .then(res => {
config: {} 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,
});
});
};
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,
});
}
});
};
componentDidMount() { updateFavicon = () => {
this.updateFavicon(); const link =
axios.get( document.querySelector("link[rel*='icon']") ||
window.location.origin + "/publisher/public/conf/config.json", document.createElement('link');
).then(res => { link.type = 'image/x-icon';
const config = res.data; link.rel = 'shortcut icon';
this.checkUserLoggedIn(config); link.href =
}).catch((error) => { window.location.origin +
this.setState({ '/devicemgt/public/uuf.unit.favicon/img/favicon.png';
loading: false, document.getElementsByTagName('head')[0].appendChild(link);
error: true };
})
});
}
getAndroidEnterpriseToken = (config) => { render() {
axios.get( const { loading, error } = this.state;
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
})
});
};
checkUserLoggedIn = (config) => { const applicationView = (
axios.post( <Router>
window.location.origin + "/publisher-ui-request-handler/user", <ConfigContext.Provider value={this.state.config}>
"platform=publisher" <div>
).then(res => { <Switch>
config.user = res.data.data; <Redirect exact from="/publisher" to="/publisher/apps" />
const pageURL = window.location.pathname; {this.props.routes.map(route => (
const lastURLSegment = pageURL.substr(pageURL.lastIndexOf('/') + 1); <RouteWithSubRoutes key={route.path} {...route} />
if (lastURLSegment === "login") { ))}
window.location.href = window.location.origin + `/publisher/`; </Switch>
} else { </div>
this.getAndroidEnterpriseToken(config); </ConfigContext.Provider>
} </Router>
}).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 = () =>{ return (
const link = document.querySelector("link[rel*='icon']") || document.createElement('link'); <div>
link.type = 'image/x-icon'; {loading && loadingView}
link.rel = 'shortcut icon'; {!loading && !error && applicationView}
link.href = window.location.origin+'/devicemgt/public/uuf.unit.favicon/img/favicon.png'; {error && errorView}
document.getElementsByTagName('head')[0].appendChild(link); </div>
}; );
}
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>
);
return (
<div>
{loading && loadingView}
{!loading && !error && applicationView}
{error && errorView}
</div>
);
}
} }
export default App; export default App;

View File

@ -17,21 +17,24 @@
*/ */
import React from 'react'; import React from 'react';
import {Route} from 'react-router-dom'; import { Route } from 'react-router-dom';
class RouteWithSubRoutes extends React.Component{ class RouteWithSubRoutes extends React.Component {
props; props;
constructor(props){ constructor(props) {
super(props); super(props);
this.props = props; this.props = props;
} }
render() { render() {
return( return (
<Route path={this.props.path} exact={this.props.exact} render={(props) => ( <Route
<this.props.component {...props} routes={this.props.routes}/> path={this.props.path}
)}/> exact={this.props.exact}
); render={props => (
} <this.props.component {...props} routes={this.props.routes} />
)}
/>
);
}
} }
export default RouteWithSubRoutes; export default RouteWithSubRoutes;

View File

@ -16,124 +16,161 @@
* under the License. * under the License.
*/ */
import React from "react"; import React from 'react';
import {Row, Typography, Icon, notification} from "antd"; import { Row, Typography, Icon } from 'antd';
import StarRatings from "react-star-ratings"; import StarRatings from 'react-star-ratings';
import "./DetailedRating.css"; import './DetailedRating.css';
import axios from "axios"; import axios from 'axios';
import {withConfigContext} from "../../../context/ConfigContext"; import { withConfigContext } from '../../../context/ConfigContext';
import {handleApiError} from "../../../js/Utils"; import { handleApiError } from '../../../js/Utils';
const { Text } = Typography; const { Text } = Typography;
class DetailedRating extends React.Component {
class DetailedRating extends React.Component{ constructor(props) {
super(props);
constructor(props){ this.state = {
super(props); detailedRating: null,
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);
});
}; };
}
render() { componentDidMount() {
const detailedRating = this.state.detailedRating; const { type, uuid } = this.props;
this.getData(type, uuid);
}
componentDidUpdate(prevProps, prevState) {
if(detailedRating ==null){ if (prevProps.uuid !== this.props.uuid) {
return null; const { type, uuid } = this.props;
} this.getData(type, uuid);
const totalCount = detailedRating.noOfUsers;
const ratingVariety = detailedRating.ratingVariety;
const ratingArray = [];
for (let [key, value] of Object.entries(ratingVariety)) {
ratingArray.push(value);
}
const maximumRating = Math.max(...ratingArray);
const ratingBarPercentages = [0,0,0,0,0];
if(maximumRating>0){
for(let i = 0; i<5; i++){
ratingBarPercentages[i] = (ratingVariety[(i+1).toString()])/maximumRating*100;
}
}
return (
<Row className="d-rating">
<div className="numeric-data">
<div className="rate">{detailedRating.ratingValue.toFixed(1)}</div>
<StarRatings
rating={detailedRating.ratingValue}
starRatedColor="#777"
starDimension = "16px"
starSpacing = "2px"
numberOfStars={5}
name='rating'
/>
<br/>
<Text type="secondary" className="people-count"><Icon type="team" /> {totalCount} total</Text>
</div>
<div className="bar-containers">
<div className="bar-container">
<span className="number">5</span>
<span className="bar rate-5" style={{width: ratingBarPercentages[4]+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">4</span>
<span className="bar rate-4" style={{width: ratingBarPercentages[3]+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">3</span>
<span className="bar rate-3" style={{width: ratingBarPercentages[2]+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">2</span>
<span className="bar rate-2" style={{width: ratingBarPercentages[1]+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">1</span>
<span className="bar rate-1" style={{width: ratingBarPercentages[0]+"%"}}> </span>
</div>
</div>
</Row>
);
} }
}
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,
);
});
};
render() {
const detailedRating = this.state.detailedRating;
if (detailedRating == null) {
return null;
}
const totalCount = detailedRating.noOfUsers;
const ratingVariety = detailedRating.ratingVariety;
const ratingArray = [];
// eslint-disable-next-line no-unused-vars
for (let [key, value] of Object.entries(ratingVariety)) {
ratingArray.push(value);
}
const maximumRating = Math.max(...ratingArray);
const ratingBarPercentages = [0, 0, 0, 0, 0];
if (maximumRating > 0) {
for (let i = 0; i < 5; i++) {
ratingBarPercentages[i] =
(ratingVariety[(i + 1).toString()] / maximumRating) * 100;
}
}
return (
<Row className="d-rating">
<div className="numeric-data">
<div className="rate">{detailedRating.ratingValue.toFixed(1)}</div>
<StarRatings
rating={detailedRating.ratingValue}
starRatedColor="#777"
starDimension="16px"
starSpacing="2px"
numberOfStars={5}
name="rating"
/>
<br />
<Text type="secondary" className="people-count">
<Icon type="team" /> {totalCount} total
</Text>
</div>
<div className="bar-containers">
<div className="bar-container">
<span className="number">5</span>
<span
className="bar rate-5"
style={{ width: ratingBarPercentages[4] + '%' }}
>
{' '}
</span>
</div>
<div className="bar-container">
<span className="number">4</span>
<span
className="bar rate-4"
style={{ width: ratingBarPercentages[3] + '%' }}
>
{' '}
</span>
</div>
<div className="bar-container">
<span className="number">3</span>
<span
className="bar rate-3"
style={{ width: ratingBarPercentages[2] + '%' }}
>
{' '}
</span>
</div>
<div className="bar-container">
<span className="number">2</span>
<span
className="bar rate-2"
style={{ width: ratingBarPercentages[1] + '%' }}
>
{' '}
</span>
</div>
<div className="bar-container">
<span className="number">1</span>
<span
className="bar rate-1"
style={{ width: ratingBarPercentages[0] + '%' }}
>
{' '}
</span>
</div>
</div>
</Row>
);
}
} }
export default withConfigContext(DetailedRating); export default withConfigContext(DetailedRating);

View File

@ -16,314 +16,327 @@
* under the License. * under the License.
*/ */
import React from "react"; import React from 'react';
import { import {
Card, Card,
Col, Col,
Row, Row,
Typography, Typography,
Input, Divider,
Divider, Select,
Icon, Button,
Select, Form,
Button, Alert,
Form, } from 'antd';
message, import axios from 'axios';
Radio, import { withConfigContext } from '../../../context/ConfigContext';
notification, Alert import { handleApiError } from '../../../js/Utils';
} from "antd";
import axios from "axios";
import {withConfigContext} from "../../../context/ConfigContext";
import {handleApiError} from "../../../js/Utils";
const {Option} = Select;
const {Title} = Typography;
const { Option } = Select;
const { Title } = Typography;
class FiltersForm extends React.Component { class FiltersForm extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
categories: [], categories: [],
tags: [], tags: [],
deviceTypes: [], deviceTypes: [],
forbiddenErrors: { forbiddenErrors: {
categories: false, categories: false,
tags: false, tags: false,
deviceTypes: 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);
});
}; };
}
componentDidMount() { handleSubmit = e => {
this.getCategories(); e.preventDefault();
this.getTags(); this.props.form.validateFields((err, values) => {
this.getDeviceTypes(); for (const [key, value] of Object.entries(values)) {
} if (value === undefined) {
delete values[key];
}
}
getCategories = () => { if (values.hasOwnProperty('deviceType') && values.deviceType === 'ALL') {
const config = this.props.context; delete values.deviceType;
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) => { if (values.hasOwnProperty('appType') && values.appType === 'ALL') {
handleApiError(error, "Error occurred while trying to load categories.", true); delete values.appType;
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
});
}
});
};
getTags = () => { this.props.setFilters(values);
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) => { componentDidMount() {
handleApiError(error, "Error occurred while trying to load tags.", true); this.getCategories();
if (error.hasOwnProperty("response") && error.response.status === 403) { this.getTags();
const {forbiddenErrors} = this.state; this.getDeviceTypes();
forbiddenErrors.tags = true; }
this.setState({
forbiddenErrors,
loading: false
})
} else {
this.setState({
loading: false
});
}
});
};
getCategories = () => {
getDeviceTypes = () => { const config = this.props.context;
const config = this.props.context; axios
axios.get( .get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt + "/device-types" window.location.origin +
).then(res => { config.serverConfig.invoker.uri +
if (res.status === 200) { config.serverConfig.invoker.publisher +
const deviceTypes = JSON.parse(res.data.data); '/applications/categories',
this.setState({ )
deviceTypes, .then(res => {
loading: false, if (res.status === 200) {
}); let categories = JSON.parse(res.data.data);
} this.setState({
categories: categories,
}).catch((error) => { loading: false,
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; .catch(error => {
this.setState({ handleApiError(
forbiddenErrors, error,
loading: false 'Error occurred while trying to load categories.',
}) true,
} else {
this.setState({
loading: false
});
}
});
};
render() {
const {categories, tags, deviceTypes, forbiddenErrors} = this.state;
const {getFieldDecorator} = this.props.form;
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>
<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>
); );
} 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,
});
}
});
};
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,
});
}
});
};
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,
});
}
});
};
render() {
const { categories, tags, deviceTypes, forbiddenErrors } = this.state;
const { getFieldDecorator } = this.props.form;
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>
<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(
const Filters = withConfigContext(Form.create({name: 'filter-apps'})(FiltersForm)); Form.create({ name: 'filter-apps' })(FiltersForm),
);
export default withConfigContext(Filters); export default withConfigContext(Filters);

View File

@ -16,89 +16,87 @@
* under the License. * under the License.
*/ */
import React from "react"; import React from 'react';
import {Card, Col, Row, Typography, Input, Divider, notification} from "antd"; import { Card, Col, Row, Typography, Input, Divider } from 'antd';
import AppsTable from "./appsTable/AppsTable"; import AppsTable from './appsTable/AppsTable';
import Filters from "./Filters"; import Filters from './Filters';
import AppDetailsDrawer from "./AppDetailsDrawer/AppDetailsDrawer";
import axios from "axios";
const {Title} = Typography; const { Title } = Typography;
const Search = Input.Search; const Search = Input.Search;
class ListApps extends React.Component { class ListApps extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
filters: {} filters: {},
}; };
this.appName = ''; this.appName = '';
}
setFilters = filters => {
if (this.appName === '' && filters.hasOwnProperty('appName')) {
delete filters.appName;
} else {
filters.appName = this.appName;
} }
this.setState({
filters,
});
};
setFilters = (filters) => { setSearchText = appName => {
if (this.appName === '' && filters.hasOwnProperty("appName")) { const filters = { ...this.state.filters };
delete filters["appName"]; this.appName = appName;
} else { if (appName === '' && filters.hasOwnProperty('appName')) {
filters.appName = this.appName; delete filters.appName;
} } else {
this.setState({ filters.appName = appName;
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
});
};
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 {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>
);
} }
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,
});
}
};
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; export default ListApps;

View File

@ -16,292 +16,321 @@
* under the License. * under the License.
*/ */
import React from "react"; import React from 'react';
import {Avatar, Table, Tag, Icon, message, notification, Col, Badge, Alert, Tooltip} from "antd"; import { Avatar, Table, Tag, Icon, Badge, Alert, Tooltip } from 'antd';
import axios from "axios"; import axios from 'axios';
import pSBC from 'shade-blend-color'; import pSBC from 'shade-blend-color';
import "./AppsTable.css"; import './AppsTable.css';
import {withConfigContext} from "../../../../context/ConfigContext"; import { withConfigContext } from '../../../../context/ConfigContext';
import AppDetailsDrawer from "../AppDetailsDrawer/AppDetailsDrawer"; import AppDetailsDrawer from '../AppDetailsDrawer/AppDetailsDrawer';
import {handleApiError} from "../../../../js/Utils"; import { handleApiError } from '../../../../js/Utils';
let config = null; let config = null;
const columns = [ const columns = [
{ {
title: '', title: '',
dataIndex: 'name', dataIndex: 'name',
render: (name, row) => { // eslint-disable-next-line react/display-name
let avatar = null; render: (name, row) => {
if (row.applicationReleases.length === 0) { let avatar = null;
const avatarLetter = name.charAt(0).toUpperCase(); if (row.applicationReleases.length === 0) {
avatar = ( const avatarLetter = name.charAt(0).toUpperCase();
<Avatar shape="square" size="large" avatar = (
style={{ <Avatar
marginRight: 20, shape="square"
borderRadius: "28%", size="large"
border: "1px solid #ddd", style={{
backgroundColor: pSBC(0.50, config.theme.primaryColor) marginRight: 20,
}}> borderRadius: '28%',
{avatarLetter} border: '1px solid #ddd',
</Avatar> backgroundColor: pSBC(0.5, config.theme.primaryColor),
); }}
} else { >
const {applicationReleases} = row; {avatarLetter}
let hasPublishedRelease = false; </Avatar>
for (let i = 0; i < applicationReleases.length; i++) { );
if (applicationReleases[i].currentStatus === "PUBLISHED") { } else {
hasPublishedRelease = true; const { applicationReleases } = row;
break; let hasPublishedRelease = false;
} for (let i = 0; i < applicationReleases.length; i++) {
} if (applicationReleases[i].currentStatus === 'PUBLISHED') {
avatar = (hasPublishedRelease) ? ( hasPublishedRelease = true;
<Badge break;
title="Published" }
style={{backgroundColor: '#52c41a', borderRadius: "50%", color: "white"}} }
count={ avatar = hasPublishedRelease ? (
<Tooltip <Badge
title="Published"> title="Published"
<Icon style={{
style={{ backgroundColor: '#52c41a',
backgroundColor: '#52c41a', borderRadius: '50%',
borderRadius: "50%", color: 'white',
color: "white" }}
}} count={
type="check-circle"/> <Tooltip title="Published">
</Tooltip> <Icon
}> style={{
<Avatar shape="square" size="large" backgroundColor: '#52c41a',
style={{ borderRadius: '50%',
borderRadius: "28%", color: 'white',
border: "1px solid #ddd" }}
}} type="check-circle"
src={row.applicationReleases[0].iconPath} />
/> </Tooltip>
</Badge>
) : (
<Avatar shape="square" size="large"
style={{
borderRadius: "28%",
border: "1px solid #ddd"
}}
src={row.applicationReleases[0].iconPath}
/>
);
} }
>
<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 ( return (
<div> <div>
{avatar} {avatar}
<span style={{marginLeft: 20}}>{name}</span> <span style={{ marginLeft: 20 }}>{name}</span>
</div>); </div>
} );
}, },
{ },
title: 'Categories', {
dataIndex: 'categories', title: 'Categories',
render: categories => ( dataIndex: 'categories',
<span> // eslint-disable-next-line react/display-name
{categories.map(category => { render: categories => (
return ( <span>
<Tag {categories.map(category => {
style={{marginBottom: 8}} return (
color={pSBC(0.30, config.theme.primaryColor)} <Tag
key={category}> style={{ marginBottom: 8 }}
{category} color={pSBC(0.3, config.theme.primaryColor)}
</Tag> key={category}
); >
})} {category}
</span> </Tag>
) );
}, })}
{ </span>
title: 'Platform', ),
dataIndex: 'deviceType', },
render: platform => { {
const defaultPlatformIcons = config.defaultPlatformIcons; title: 'Platform',
let icon = defaultPlatformIcons.default.icon; dataIndex: 'deviceType',
let color = defaultPlatformIcons.default.color; // eslint-disable-next-line react/display-name
let theme = defaultPlatformIcons.default.theme; render: platform => {
if (defaultPlatformIcons.hasOwnProperty(platform)) { const defaultPlatformIcons = config.defaultPlatformIcons;
icon = defaultPlatformIcons[platform].icon; let icon = defaultPlatformIcons.default.icon;
color = defaultPlatformIcons[platform].color; let color = defaultPlatformIcons.default.color;
theme = defaultPlatformIcons[platform].theme; let theme = defaultPlatformIcons.default.theme;
} if (defaultPlatformIcons.hasOwnProperty(platform)) {
return ( icon = defaultPlatformIcons[platform].icon;
<span style={{fontSize: 20, color: color, textAlign: "center"}}> color = defaultPlatformIcons[platform].color;
<Icon type={icon} theme={theme}/> theme = defaultPlatformIcons[platform].theme;
</span> }
); return (
} <span style={{ fontSize: 20, color: color, textAlign: 'center' }}>
}, <Icon type={icon} theme={theme} />
{ </span>
title: 'Type', );
dataIndex: 'type'
},
{
title: 'Subscription',
dataIndex: 'subMethod'
}, },
},
{
title: 'Type',
dataIndex: 'type',
},
{
title: 'Subscription',
dataIndex: 'subMethod',
},
]; ];
class AppsTable extends React.Component { class AppsTable extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
pagination: {}, pagination: {},
apps: [], apps: [],
filters: {}, filters: {},
isDrawerVisible: false, isDrawerVisible: false,
selectedApp: null, selectedApp: null,
selectedAppIndex: -1, selectedAppIndex: -1,
loading: false,
isForbiddenErrorVisible: false,
};
config = this.props.context;
}
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);
}
}
// 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,
});
};
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,
});
};
fetch = (filters, params = {}) => {
this.setState({ loading: true });
const config = this.props.context;
if (!params.hasOwnProperty('page')) {
params.page = 1;
}
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 (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, loading: false,
isForbiddenErrorVisible: false apps: apps,
}; pagination,
config = this.props.context; });
}
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);
} }
} })
.catch(error => {
//handler to show app drawer handleApiError(
showDrawer = (app, appIndex) => { error,
this.setState({ 'Error occurred while trying to load apps.',
isDrawerVisible: true, true,
selectedApp: app,
selectedAppIndex: appIndex
});
};
// handler to close the app drawer
closeDrawer = () => {
this.setState({
isDrawerVisible: false
})
};
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,
});
};
fetch = (filters, params = {}) => {
this.setState({loading: true});
const config = this.props.context;
if (!params.hasOwnProperty("page")) {
params.page = 1;
}
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 (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>
); );
} 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);

View File

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

View File

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

View File

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

View File

@ -16,147 +16,166 @@
* under the License. * under the License.
*/ */
import React from "react"; import React from 'react';
import {List, message, Avatar, Spin, Button, notification, Alert} from 'antd'; import { List, Spin, Button, Alert } from 'antd';
import "./Reviews.css"; import './Reviews.css';
import InfiniteScroll from 'react-infinite-scroller'; import InfiniteScroll from 'react-infinite-scroller';
import SingleReview from "./SingleReview"; import SingleReview from './SingleReview';
import axios from "axios"; import axios from 'axios';
import {withConfigContext} from "../../../../context/ConfigContext"; import { withConfigContext } from '../../../../context/ConfigContext';
import {handleApiError} from "../../../../js/Utils"; import { handleApiError } from '../../../../js/Utils';
const limit = 5; const limit = 5;
class Reviews extends React.Component { class Reviews extends React.Component {
state = { state = {
data: [], data: [],
loading: false, loading: false,
hasMore: false, hasMore: false,
loadMore: false, loadMore: false,
forbiddenErrors: { forbiddenErrors: {
reviews: false reviews: false,
},
};
componentDidMount() {
this.fetchData(0, limit, res => {
this.setState({
data: res,
});
});
}
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(
componentDidMount() { error,
this.fetchData(0, limit, res => { 'Error occurred while trying to load reviews.',
this.setState({ true,
data: res,
});
});
}
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
});
}
});
};
handleInfiniteOnLoad = (count) => {
const offset = count * limit;
let data = this.state.data;
this.setState({
loading: true,
});
if (data.length > 149) {
this.setState({
hasMore: false,
loading: false,
});
return;
}
this.fetchData(offset, limit, res => {
if (res.length > 0) {
data = data.concat(res);
this.setState({
data,
loading: false,
});
} else {
this.setState({
hasMore: false,
loading: false
});
}
});
};
enableLoading = () => {
this.setState({
hasMore: true,
loadMore: true
});
};
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>
); );
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,
});
}
});
};
handleInfiniteOnLoad = count => {
const offset = count * limit;
let data = this.state.data;
this.setState({
loading: true,
});
if (data.length > 149) {
this.setState({
hasMore: false,
loading: false,
});
return;
} }
this.fetchData(offset, limit, res => {
if (res.length > 0) {
data = data.concat(res);
this.setState({
data,
loading: false,
});
} else {
this.setState({
hasMore: false,
loading: false,
});
}
});
};
enableLoading = () => {
this.setState({
hasMore: true,
loadMore: true,
});
};
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); export default withConfigContext(Reviews);

View File

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

View File

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

View File

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

View File

@ -16,183 +16,215 @@
* under the License. * under the License.
*/ */
import React from "react"; import React from 'react';
import {Button, message, Modal, notification, Spin} from "antd"; import { Button, Modal, notification, Spin } from 'antd';
import axios from "axios"; import axios from 'axios';
import {withConfigContext} from "../../../../context/ConfigContext"; import { withConfigContext } from '../../../../context/ConfigContext';
import {handleApiError} from "../../../../js/Utils"; import { handleApiError } from '../../../../js/Utils';
// import gapi from 'gapi-client';
class ManagedConfigurationsIframe extends React.Component { class ManagedConfigurationsIframe extends React.Component {
constructor(props) {
constructor(props) { super(props);
super(props); this.config = this.props.context;
this.config = this.props.context; this.state = {
this.state = { visible: false,
visible: false, loading: false,
loading: false
};
}
showModal = () => {
this.getMcm();
this.setState({
visible: true,
});
}; };
}
handleOk = e => { showModal = () => {
this.setState({ this.getMcm();
visible: false, this.setState({
}); visible: true,
}; });
};
handleCancel = e => { handleOk = e => {
this.setState({ this.setState({
visible: false, visible: false,
}); });
}; };
getMcm = () => { handleCancel = e => {
const {packageName} = this.props; this.setState({
this.setState({loading: true}); visible: false,
});
};
//send request to the invoker getMcm = () => {
axios.get( const { packageName } = this.props;
window.location.origin + this.config.serverConfig.invoker.uri + this.setState({ loading: true });
"/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) => { // send request to the invoker
handleApiError(error, "Error occurred while trying to load configurations."); axios
this.setState({loading: false, visible: false}); .get(
}); window.location.origin +
}; this.config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/managed-configs/package/' +
loadIframe = (mcmId) => { packageName,
const {packageName} = this.props; )
let method = "post"; .then(res => {
gapi.load('gapi.iframes', () => { if (res.status === 200) {
const parameters = { let mcmId = null;
token: this.config.androidEnterpriseToken, if (res.data.hasOwnProperty('data')) {
packageName: packageName mcmId = res.data.data.mcmId;
}; }
if (mcmId != null) { this.loadIframe(mcmId);
parameters.mcmId = mcmId; this.setState({ loading: false });
parameters.canDelete = true; }
method = "put"; })
} .catch(error => {
handleApiError(
const queryString = Object.keys(parameters).map(key => key + '=' + parameters[key]).join('&'); error,
'Error occurred while trying to load configurations.',
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>
); );
} 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,
};
// 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 => {
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,452 +16,507 @@
* under the License. * under the License.
*/ */
import React from "react"; import React from 'react';
import { import {
Card, Card,
Tag, Tag,
message, message,
Icon, Icon,
Input, Input,
notification, notification,
Divider, Divider,
Button, Button,
Spin, Spin,
Tooltip, Tooltip,
Popconfirm, Popconfirm,
Modal, Modal,
Row, Row,
Col, Col,
Typography, Alert Typography,
} from "antd"; Alert,
import axios from "axios"; } from 'antd';
import {TweenOneGroup} from 'rc-tween-one'; import axios from 'axios';
import pSBC from "shade-blend-color"; import { TweenOneGroup } from 'rc-tween-one';
import {withConfigContext} from "../../../context/ConfigContext"; import pSBC from 'shade-blend-color';
import {handleApiError} from "../../../js/Utils"; import { withConfigContext } from '../../../context/ConfigContext';
import { handleApiError } from '../../../js/Utils';
const {Title} = Typography; const { Title } = Typography;
class ManageCategories extends React.Component { class ManageCategories extends React.Component {
state = { state = {
loading: false, loading: false,
searchText: '', searchText: '',
categories: [], categories: [],
tempElements: [], tempElements: [],
inputVisible: false, inputVisible: false,
inputValue: '', inputValue: '',
isAddNewVisible: false, isAddNewVisible: false,
isEditModalVisible: false, isEditModalVisible: false,
currentlyEditingId: null, currentlyEditingId: null,
editingValue: null, editingValue: null,
forbiddenErrors: { forbiddenErrors: {
categories: false 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,
});
} }
}; })
.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,
});
}
});
}
componentDidMount() { handleCloseButton = () => {
const config = this.props.context; this.setState({
axios.get( tempElements: [],
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications/categories", isAddNewVisible: false,
).then(res => { });
if (res.status === 200) { };
let categories = JSON.parse(res.data.data);
this.setState({
categories: categories,
loading: false
});
}
}).catch((error) => { deleteCategory = id => {
handleApiError(error, "Error occured while trying to load categories", true); const config = this.props.context;
if (error.hasOwnProperty("response") && error.response.status === 403) { this.setState({
const {forbiddenErrors} = this.state; loading: true,
forbiddenErrors.categories = true; });
this.setState({ axios
forbiddenErrors, .delete(
loading: false window.location.origin +
}) config.serverConfig.invoker.uri +
} else { config.serverConfig.invoker.publisher +
this.setState({ '/admin/applications/categories/' +
loading: false 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.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.',
});
}
}}
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');
}
} }
handleCloseButton = () => { this.setState({
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: [], 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>
);
};
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, inputVisible: false,
inputValue: '', inputValue: '',
}); loading: false,
}; isAddNewVisible: false,
});
handleSave = () => { }
const config = this.props.context; })
const {tempElements, categories} = this.state; .catch(error => {
handleApiError(error, 'Error occurred while trying to add categories.');
this.setState({ this.setState({
loading: true loading: false,
}); });
});
};
const data = tempElements.map(category => category.categoryName); saveInputRef = input => (this.input = input);
axios.post( closeEditModal = e => {
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/admin/applications/categories", this.setState({
data, isEditModalVisible: false,
).then(res => { currentlyEditingId: null,
if (res.status === 200) { });
notification["success"]({ };
message: "Done!",
description:
"New Categories were added successfully",
});
this.setState({ openEditModal = id => {
categories: [...categories, ...tempElements], this.setState({
tempElements: [], isEditModalVisible: true,
inputVisible: false, currentlyEditingId: id,
inputValue: '', editingValue: id,
loading: false, });
isAddNewVisible: false };
});
}
}).catch((error) => { editItem = () => {
handleApiError(error, "Error occurred while trying to add categories."); const config = this.props.context;
this.setState({
loading: false
});
});
const { editingValue, currentlyEditingId, categories } = this.state;
}; this.setState({
loading: true,
isEditModalVisible: false,
});
saveInputRef = input => (this.input = input); 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',
});
closeEditModal = e => { categories[
this.setState({ categories.findIndex(i => i.categoryName === currentlyEditingId)
isEditModalVisible: false, ].categoryName = editingValue;
currentlyEditingId: null
});
};
openEditModal = (id) => { this.setState({
this.setState({ categories: categories,
isEditModalVisible: true, loading: false,
currentlyEditingId: id, editingValue: null,
editingValue: id });
}) }
}; })
.catch(error => {
editItem = () => { handleApiError(
const config = this.props.context; error,
'Error occurred while trying to delete the category.',
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({
loading: false,
editingValue: null
});
});
};
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>
}
</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>
); );
} this.setState({
loading: false,
editingValue: null,
});
});
};
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>
)}
</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); export default withConfigContext(ManageCategories);

View File

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

View File

@ -16,209 +16,231 @@
* under the License. * under the License.
*/ */
import React from "react"; import React from 'react';
import { import { Card, Button, Steps, Row, Col, Form, Result, Spin } from 'antd';
Card, import axios from 'axios';
Button, import { withRouter } from 'react-router-dom';
Steps, import NewAppDetailsForm from './subForms/NewAppDetailsForm';
Row, import NewAppUploadForm from './subForms/NewAppUploadForm';
Col, import { withConfigContext } from '../../context/ConfigContext';
Form, import { handleApiError } from '../../js/Utils';
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;
const { Step } = Steps;
class AddNewAppFormComponent extends React.Component { 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) { onSuccessApplicationData = application => {
super(props); const { formConfig } = this.props;
this.state = { if (
current: 0, application.hasOwnProperty('deviceType') &&
categories: [], formConfig.installationType !== 'WEB_CLIP' &&
tags: [], formConfig.installationType !== 'CUSTOM'
icons: [], ) {
screenshots: [], 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, loading: false,
binaryFiles: [], current: 2,
application: null, });
release: null, } else {
isError: false, this.setState({
deviceType: null, loading: false,
supportedOsVersions: [], isError: true,
errorText: "", current: 2,
forbiddenErrors: { });
supportedOsVersions: false
}
};
}
onSuccessApplicationData = (application) => {
const {formConfig} = this.props;
if (application.hasOwnProperty("deviceType") &&
formConfig.installationType !== "WEB_CLIP" &&
formConfig.installationType !== "CUSTOM") {
this.getSupportedOsVersions(application.deviceType);
} }
})
.catch(error => {
handleApiError(error, error.response.data.data);
this.setState({ this.setState({
application, loading: false,
current: 1 isError: true,
current: 2,
errorText: error.response.data.data,
}); });
}; });
};
onSuccessReleaseData = (releaseData) => { onClickBackButton = () => {
const config = this.props.context; const current = this.state.current - 1;
this.setState({ this.setState({ current });
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
});
});
};
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>
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); export default withConfigContext(AddNewAppForm);

View File

@ -16,430 +16,481 @@
* under the License. * under the License.
*/ */
import React from "react"; import React from 'react';
import {Alert, Button, Col, Divider, Form, Icon, Input, notification, Row, Select, Spin, Switch, Upload} from "antd"; import { Alert, Button, Col, Form, Input, Row, Select, Spin } from 'antd';
import axios from "axios"; import axios from 'axios';
import {withConfigContext} from "../../../context/ConfigContext"; import { withConfigContext } from '../../../context/ConfigContext';
import {handleApiError} from "../../../js/Utils"; import { handleApiError } from '../../../js/Utils';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
const formItemLayout = { const formItemLayout = {
labelCol: { labelCol: {
xs: {span: 24}, xs: { span: 24 },
sm: {span: 5}, sm: { span: 5 },
}, },
wrapperCol: { wrapperCol: {
xs: {span: 24}, xs: { span: 24 },
sm: {span: 19}, sm: { span: 19 },
}, },
}; };
const {Option} = Select; const { Option } = Select;
const {TextArea} = Input; const { TextArea } = Input;
class NewAppDetailsForm extends React.Component { class NewAppDetailsForm extends React.Component {
constructor(props) {
constructor(props) { super(props);
super(props); this.state = {
this.state = { categories: [],
categories: [], tags: [],
tags: [], deviceTypes: [],
deviceTypes: [], fetching: false,
fetching: false, roleSearchValue: [],
roleSearchValue: [], unrestrictedRoles: [],
unrestrictedRoles: [], forbiddenErrors: {
forbiddenErrors: { categories: false,
categories: false, tags: false,
tags: false, deviceTypes: false,
deviceTypes: false, roles: 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";
}
this.props.onSuccessApplicationData(application);
}
});
}; };
this.lastFetchId = 0;
this.fetchRoles = debounce(this.fetchRoles, 800);
}
componentDidMount() { handleSubmit = e => {
this.getCategories(); e.preventDefault();
this.getTags(); const { formConfig } = this.props;
this.getDeviceTypes();
}
getCategories = () => { this.props.form.validateFields((err, values) => {
const config = this.props.context; if (!err) {
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
});
}
});
};
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
});
}
});
};
getDeviceTypes = () => {
const config = this.props.context;
const {formConfig} = this.props;
const {installationType} = formConfig;
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 = [];
// 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);
}
});
}
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
});
}
});
};
fetchRoles = value => {
const config = this.props.context;
this.lastFetchId += 1;
const fetchId = this.lastFetchId;
this.setState({data: [], fetching: true});
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt + "/roles?filter=" + value,
).then(res => {
if (res.status === 200) {
if (fetchId !== this.lastFetchId) {
// for fetch callback order
return;
}
const data = res.data.data.roles.map(role => ({
text: role,
value: role,
}));
this.setState({
unrestrictedRoles: data,
fetching: false
});
}
}).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
});
}
});
};
handleRoleSearch = roleSearchValue => {
this.setState({ this.setState({
roleSearchValue, loading: true,
unrestrictedRoles: [],
fetching: false,
}); });
}; const {
name,
description,
categories,
tags,
unrestrictedRoles,
} = values;
const unrestrictedRolesData = [];
unrestrictedRoles.map(val => {
unrestrictedRolesData.push(val.key);
});
const application = {
name,
description,
categories,
tags,
unrestrictedRoles: unrestrictedRolesData,
};
render() { if (formConfig.installationType !== 'WEB_CLIP') {
const {formConfig} = this.props; application.deviceType = values.deviceType;
const {categories, tags, deviceTypes, fetching, roleSearchValue, unrestrictedRoles, forbiddenErrors} = this.state; } else {
const {getFieldDecorator} = this.props.form; application.type = 'WEB_CLIP';
application.deviceType = 'ALL';
}
return ( this.props.onSuccessApplicationData(application);
<div> }
<Row> });
<Col md={5}> };
</Col> componentDidMount() {
<Col md={14}> this.getCategories();
<Form this.getTags();
labelAlign="right" this.getDeviceTypes();
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>
)}
{/*app name*/} getCategories = () => {
<Form.Item {...formItemLayout} label="App Name"> const config = this.props.context;
{getFieldDecorator('name', { axios
rules: [{ .get(
required: true, window.location.origin +
message: 'Please input a name' config.serverConfig.invoker.uri +
}], config.serverConfig.invoker.publisher +
})( '/applications/categories',
<Input placeholder="ex: Lorem App"/> )
)} .then(res => {
</Form.Item> if (res.status === 200) {
let categories = JSON.parse(res.data.data);
{/*description*/} this.setState({
<Form.Item {...formItemLayout} label="Description"> categories: categories,
{getFieldDecorator('description', { loading: false,
rules: [{ });
required: true, }
message: 'Please enter a description' })
}], .catch(error => {
})( handleApiError(
<TextArea placeholder="Enter the description..." rows={7}/> error,
)} 'Error occurred while trying to load categories.',
</Form.Item> true,
{/*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>
); );
} 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,
});
}
});
};
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,
});
}
});
};
getDeviceTypes = () => {
const config = this.props.context;
const { formConfig } = this.props;
const { installationType } = formConfig;
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 = [];
// 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);
}
});
}
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,
});
}
});
};
fetchRoles = value => {
const config = this.props.context;
this.lastFetchId += 1;
const fetchId = this.lastFetchId;
this.setState({ data: [], fetching: true });
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
config.serverConfig.invoker.deviceMgt +
'/roles?filter=' +
value,
)
.then(res => {
if (res.status === 200) {
if (fetchId !== this.lastFetchId) {
// for fetch callback order
return;
}
const data = res.data.data.roles.map(role => ({
text: role,
value: role,
}));
this.setState({
unrestrictedRoles: data,
fetching: false,
});
}
})
.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,
});
}
});
};
handleRoleSearch = roleSearchValue => {
this.setState({
roleSearchValue,
unrestrictedRoles: [],
fetching: false,
});
};
render() {
const { formConfig } = this.props;
const {
categories,
tags,
deviceTypes,
fetching,
unrestrictedRoles,
forbiddenErrors,
} = this.state;
const { getFieldDecorator } = this.props.form;
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>
)}
{/* 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>
{/* 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),
);

View File

@ -16,146 +16,162 @@
* under the License. * under the License.
*/ */
import React from "react"; import React from 'react';
import {Form, notification, Spin, Card, Row, Col} from "antd"; import { Form, notification, Spin, Card, Row, Col } from 'antd';
import axios from "axios"; import axios from 'axios';
import {withRouter} from 'react-router-dom' import { withRouter } from 'react-router-dom';
import {withConfigContext} from "../../context/ConfigContext"; import { withConfigContext } from '../../context/ConfigContext';
import {handleApiError} from "../../js/Utils"; import { handleApiError } from '../../js/Utils';
import NewAppUploadForm from "../new-app/subForms/NewAppUploadForm"; import NewAppUploadForm from '../new-app/subForms/NewAppUploadForm';
const formConfig = { const formConfig = {
specificElements: { specificElements: {
binaryFile: { binaryFile: {
required: true required: true,
} },
} },
}; };
class AddNewReleaseFormComponent extends React.Component { class AddNewReleaseFormComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
supportedOsVersions: [],
application: null,
release: null,
deviceType: null,
forbiddenErrors: {
supportedOsVersions: false,
},
};
}
constructor(props) { componentDidMount() {
super(props); this.getSupportedOsVersions(this.props.deviceType);
this.state = { }
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, loading: false,
supportedOsVersions: [], });
application: null, }
release: null, })
deviceType: null, .catch(error => {
forbiddenErrors: { handleApiError(
supportedOsVersions: false error,
} 'Error occurred while trying to load supported OS versions.',
}; true,
}
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
});
}
});
};
onSuccessReleaseData = (releaseData) => {
const config = this.props.context;
const {appId, deviceType} = this.props;
this.setState({
loading: true
});
const {data, release} = releaseData;
const json = JSON.stringify(release);
const blob = new Blob([json], {
type: 'application/json'
});
data.append("applicationRelease", blob);
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/');
};
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>
); );
} 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,
});
}
});
};
onSuccessReleaseData = releaseData => {
const config = this.props.context;
const { appId, deviceType } = this.props;
this.setState({
loading: true,
});
const { data, release } = releaseData;
const json = JSON.stringify(release);
const blob = new Blob([json], {
type: 'application/json',
});
data.append('applicationRelease', blob);
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/');
};
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); export default withConfigContext(AddReleaseForm);

View File

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

View File

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

View File

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

View File

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

View File

@ -16,214 +16,247 @@
* under the License. * under the License.
*/ */
import React from "react"; import React from 'react';
import {Layout, Menu, Icon, Drawer, Button} from 'antd'; import { Layout, Menu, Icon, Drawer, Button } from 'antd';
import {Switch, Link} from "react-router-dom"; import { Switch, Link } from 'react-router-dom';
import RouteWithSubRoutes from "../../components/RouteWithSubRoutes" import RouteWithSubRoutes from '../../components/RouteWithSubRoutes';
import {Redirect} from 'react-router' import { Redirect } from 'react-router';
import "./Dashboard.css"; import './Dashboard.css';
import {withConfigContext} from "../../context/ConfigContext"; import { withConfigContext } from '../../context/ConfigContext';
import Logout from "./logout/Logout"; import Logout from './logout/Logout';
const {Header, Content, Footer} = Layout; const { Header, Content, Footer } = Layout;
const {SubMenu} = Menu; const { SubMenu } = Menu;
class Dashboard extends React.Component { class Dashboard extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
routes: props.routes, routes: props.routes,
visible: false, visible: false,
collapsed: 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
});
}; };
this.config = this.props.context;
this.Logo = this.config.theme.logo;
this.footerText = this.config.theme.footerText;
}
onCloseMobileNavigationBar = () => { showMobileNavigationBar = () => {
this.setState({ this.setState({
visible: false, visible: true,
}); collapsed: !this.state.collapsed,
}; });
};
render() { onCloseMobileNavigationBar = () => {
return ( this.setState({
<div> visible: false,
<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>
<div className="web-layout"> render() {
<Menu return (
theme="light" <div>
mode="horizontal" <Layout>
defaultSelectedKeys={['1']} <Header
style={{lineHeight: '64px'}}> style={{
<Menu.Item key="1"><Link to="/publisher/apps"><Icon paddingLeft: 0,
type="appstore"/>Apps</Link></Menu.Item> paddingRight: 0,
backgroundColor: 'white',
<SubMenu }}
title={ >
<span className="submenu-title-wrapper"> <div className="logo-image">
<Icon type="plus"/> <Link to="/publisher/apps">
Add New App <img alt="logo" src={this.Logo} />
</span> </Link>
}>
<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>
<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>
<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> </div>
);
} <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
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>
<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>
<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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,193 +16,232 @@
* under the License. * under the License.
*/ */
import React from "react"; import React from 'react';
import '../../../../App.css'; 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 axios from 'axios';
import ReleaseView from "../../../../components/apps/release/ReleaseView"; import ReleaseView from '../../../../components/apps/release/ReleaseView';
import LifeCycle from "../../../../components/apps/release/lifeCycle/LifeCycle"; import LifeCycle from '../../../../components/apps/release/lifeCycle/LifeCycle';
import {withConfigContext} from "../../../../context/ConfigContext"; import { withConfigContext } from '../../../../context/ConfigContext';
import {handleApiError} from "../../../../js/Utils"; import { handleApiError } from '../../../../js/Utils';
import NewAppUploadForm from "../../../../components/new-app/subForms/NewAppUploadForm";
const {Title} = Typography; const { Title } = Typography;
class Release extends React.Component { class Release extends React.Component {
routes; routes;
constructor(props) { constructor(props) {
super(props); super(props);
this.routes = props.routes; this.routes = props.routes;
this.state = { this.state = {
loading: true, loading: true,
app: null, app: null,
uuid: null, uuid: null,
release: null, release: null,
currentLifecycleStatus: null, currentLifecycleStatus: null,
lifecycle: null, lifecycle: null,
supportedOsVersions: [], supportedOsVersions: [],
forbiddenErrors: { forbiddenErrors: {
supportedOsVersions: false, supportedOsVersions: false,
lifeCycle: false lifeCycle: false,
} },
};
}
componentDidMount() {
const {uuid} = this.props.match.params;
this.fetchData(uuid);
this.getLifecycle();
}
changeCurrentLifecycleStatus = (status) => {
this.setState({
currentLifecycleStatus: status
});
}; };
}
updateRelease = (release) => { componentDidMount() {
this.setState({ const { uuid } = this.props.match.params;
release this.fetchData(uuid);
}); this.getLifecycle();
}; }
fetchData = (uuid) => { changeCurrentLifecycleStatus = status => {
const config = this.props.context; this.setState({
currentLifecycleStatus: status,
});
};
//send request to the invoker updateRelease = release => {
axios.get( this.setState({
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications/release/" + uuid, release,
).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) => { fetchData = uuid => {
handleApiError(error, "Error occurred while trying to load the release."); const config = this.props.context;
this.setState({loading: false});
});
};
getLifecycle = () => { // send request to the invoker
const config = this.props.context; axios
axios.get( .get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications/lifecycle-config" window.location.origin +
).then(res => { config.serverConfig.invoker.uri +
if (res.status === 200) { config.serverConfig.invoker.publisher +
const lifecycle = res.data.data; '/applications/release/' +
this.setState({ uuid,
lifecycle: lifecycle )
}) .then(res => {
} if (res.status === 200) {
const app = res.data.data;
}).catch((error) => { const release = app !== null ? app.applicationReleases[0] : null;
handleApiError(error, "Error occurred while trying to load lifecycle configuration.", true); const currentLifecycleStatus =
if (error.hasOwnProperty("response") && error.response.status === 403) { release !== null ? release.currentStatus : null;
const {forbiddenErrors} = this.state; this.setState({
forbiddenErrors.lifeCycle = true; app: app,
this.setState({ release: release,
forbiddenErrors currentLifecycleStatus: currentLifecycleStatus,
}) loading: false,
} uuid: uuid,
}); });
}; if (config.deviceTypes.mobileTypes.includes(app.deviceType)) {
this.getSupportedOsVersions(app.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
});
}
}).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 .catch(error => {
return ( handleApiError(
<div> error,
<div className="main-container"> 'Error occurred while trying to load the release.',
<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>
); );
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); export default withConfigContext(Release);

View File

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

View File

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

View File

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

View File

@ -16,371 +16,409 @@
* under the License. * under the License.
*/ */
import React from "react"; import React from 'react';
import { import {
PageHeader, PageHeader,
Typography, Typography,
Breadcrumb, Breadcrumb,
Button, Button,
Icon, Icon,
Col, Col,
Row, Row,
notification, notification,
message, message,
Spin, Spin,
Select, Tag,
Tag, Divider,
Divider } from 'antd';
} from "antd"; import { Link, withRouter } from 'react-router-dom';
import {Link, withRouter} from "react-router-dom"; import { withConfigContext } from '../../../../../context/ConfigContext';
import {withConfigContext} from "../../../../../context/ConfigContext"; import axios from 'axios';
import axios from "axios"; import Cluster from '../../../../../components/manage/android-enterprise/Pages/Cluster/Cluster';
import Cluster from "../../../../../components/manage/android-enterprise/Pages/Cluster/Cluster"; import EditLinks from '../../../../../components/manage/android-enterprise/Pages/EditLinks/EditLinks';
import EditLinks from "../../../../../components/manage/android-enterprise/Pages/EditLinks/EditLinks"; import { handleApiError } from '../../../../../js/Utils';
import {handleApiError} from "../../../../../js/Utils";
const {Option} = Select; const { Title } = Typography;
const {Title, Text} = Typography;
class Page extends React.Component { class Page extends React.Component {
routes; routes;
constructor(props) { constructor(props) {
super(props); super(props);
const {pageName, pageId} = this.props.match.params; const { pageName, pageId } = this.props.match.params;
this.pageId = pageId; this.pageId = pageId;
this.routes = props.routes; this.routes = props.routes;
this.config = this.props.context; this.config = this.props.context;
this.pages = []; this.pages = [];
this.pageNames = {}; this.pageNames = {};
this.state = { this.state = {
pageName, pageName,
clusters: [], clusters: [],
loading: false,
applications: [],
isAddNewClusterVisible: false,
links: [],
};
}
componentDidMount() {
this.fetchClusters();
this.fetchApplications();
this.fetchPages();
}
removeLoadedCluster = clusterId => {
const clusters = [...this.state.clusters];
let index = -1;
for (let i = 0; i < clusters.length; i++) {
if (clusters[i].clusterId === clusterId) {
index = i;
break;
}
}
clusters.splice(index, 1);
this.setState({
clusters,
});
};
updatePageName = pageName => {
const config = this.props.context;
if (pageName !== this.state.pageName && pageName !== '') {
const data = {
locale: 'en',
pageName: pageName,
pageId: this.pageId,
};
axios
.put(
window.location.origin +
config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/store-layout/page',
data,
)
.then(res => {
if (res.status === 200) {
notification.success({
message: 'Saved!',
description: 'Page name updated successfully!',
});
this.setState({
loading: false,
pageName: res.data.data.pageName,
});
this.props.history.push(
`/publisher/manage/android-enterprise/pages/${pageName}/${this.pageId}`,
);
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to save the page name.',
);
this.setState({ loading: false });
});
}
};
swapClusters = (index, swapIndex) => {
const clusters = [...this.state.clusters];
if (swapIndex !== -1 && index < clusters.length) {
// swap elements
[clusters[index], clusters[swapIndex]] = [
clusters[swapIndex],
clusters[index],
];
this.setState({
clusters,
});
}
};
fetchPages = () => {
const config = this.props.context;
this.setState({ loading: true });
// send request to the invoker
axios
.get(
window.location.origin +
config.serverConfig.invoker.uri +
'/device-mgt/android/v1.0/enterprise/store-layout/page',
)
.then(res => {
if (res.status === 200) {
this.pages = res.data.data.page;
let links = [];
this.pages.forEach(page => {
this.pageNames[page.id.toString()] = page.name[0].text;
if (page.id === this.pageId && page.hasOwnProperty('link')) {
links = page.link;
}
});
this.setState({
loading: false, loading: false,
applications: [], links,
isAddNewClusterVisible: false, });
links: [] }
}; })
} .catch(error => {
if (error.hasOwnProperty('response') && error.response.status === 401) {
componentDidMount() { message.error('You are not logged in');
this.fetchClusters(); window.location.href = window.location.origin + '/publisher/login';
this.fetchApplications(); } else {
this.fetchPages(); notification.error({
} message: 'There was a problem',
duration: 0,
removeLoadedCluster = (clusterId) => { description: 'Error occurred while trying to load pages.',
const clusters = [...this.state.clusters]; });
let index = -1; }
for (let i = 0; i < clusters.length; i++) {
if (clusters[i].clusterId === clusterId) { this.setState({ loading: false });
index = i; });
break; };
}
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.',
});
} }
clusters.splice(index, 1);
this.setState({ this.setState({
clusters loading: false,
}); });
});
};
// fetch applications
fetchApplications = () => {
const config = this.props.context;
this.setState({ loading: true });
const filters = {
appType: 'PUBLIC',
deviceType: 'android',
}; };
updatePageName = pageName => { // send request to the invoker
const config = this.props.context; axios
if (pageName !== this.state.pageName && pageName !== "") { .post(
const data = { window.location.origin +
locale: "en", config.serverConfig.invoker.uri +
pageName: pageName, config.serverConfig.invoker.publisher +
pageId: this.pageId '/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,
}; };
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}`); 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
} }
}).catch((error) => { />
handleApiError(error, "Error occurred while trying to save the page name.");
this.setState({loading: false});
});
}
};
swapClusters = (index, swapIndex) => {
const clusters = [...this.state.clusters];
if (swapIndex !== -1 && index < clusters.length) {
// swap elements
[clusters[index], clusters[swapIndex]] = [clusters[swapIndex], clusters[index]];
this.setState({
clusters,
});
}
};
fetchPages = () => {
const config = this.props.context;
this.setState({loading: true});
//send request to the invoker
axios.get(
window.location.origin + config.serverConfig.invoker.uri +
"/device-mgt/android/v1.0/enterprise/store-layout/page",
).then(res => {
if (res.status === 200) {
this.pages = res.data.data.page;
let links = [];
this.pages.forEach((page) => {
this.pageNames[page.id.toString()] = page.name[0]["text"];
if (page.id === this.pageId && page.hasOwnProperty("link")) {
links = page["link"];
}
});
this.setState({
loading: false,
links
});
}
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin + '/publisher/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load pages.",
});
}
this.setState({loading: false});
});
};
fetchClusters = () => {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri +
`/device-mgt/android/v1.0/enterprise/store-layout/page/${this.pageId}/clusters`
).then(res => {
if (res.status === 200) {
let clusters = JSON.parse(res.data.data);
// sort according to the orderInPage value
clusters.sort((a, b) => (a.orderInPage > b.orderInPage) ? 1 : -1);
this.setState({
clusters,
loading: false
});
}
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
window.location.href = window.location.origin + '/publisher/login';
} else if (!(error.hasOwnProperty("response") && error.response.status === 404)) {
// API sends 404 when no apps
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load clusters.",
});
}
this.setState({
loading: false
});
});
};
//fetch applications
fetchApplications = () => {
const config = this.props.context;
this.setState({loading: true});
const filters = {
appType: "PUBLIC",
deviceType: "android"
};
//send request to the invoker
axios.post(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.publisher + "/applications",
filters
).then(res => {
if (res.status === 200) {
const applications = res.data.data.applications.map(application => {
const release = application.applicationReleases[0];
return {
packageId: `app:${application.packageName}`,
iconUrl: release.iconPath,
name: application.name
}
});
this.setState({
loading: false,
applications,
});
}
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin + '/publisher/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load pages.",
});
}
this.setState({loading: false});
});
};
toggleAddNewClusterVisibility = (isAddNewClusterVisible) => {
this.setState({
isAddNewClusterVisible
});
};
addSavedClusterToThePage = (cluster) => {
this.setState({
clusters: [...this.state.clusters, cluster],
isAddNewClusterVisible: false
});
window.scrollTo(0, document.body.scrollHeight);
};
updateLinks = (links) =>{
this.setState({
links
});
};
render() {
const {pageName, loading, clusters, applications, isAddNewClusterVisible, links} = this.state;
return (
<div>
<PageHeader style={{paddingTop:0, 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>
</div> </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)); export default withConfigContext(withRouter(Page));

View File

@ -34,8 +34,8 @@ const isLocalhost = Boolean(
window.location.hostname === '[::1]' || window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4. // 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match( 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) { export function register(config) {
@ -61,7 +61,7 @@ export function register(config) {
navigator.serviceWorker.ready.then(() => { navigator.serviceWorker.ready.then(() => {
console.log( console.log(
'This web app is being served cache-first by a service ' + '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 { } else {
@ -89,7 +89,7 @@ function registerValidSW(swUrl, config) {
// content until all client tabs are closed. // content until all client tabs are closed.
console.log( console.log(
'New content is available and will be used when all ' + '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 // Execute callback
@ -139,7 +139,7 @@ function checkValidServiceWorker(swUrl, config) {
}) })
.catch(() => { .catch(() => {
console.log( console.log(
'No internet connection found. App is running in offline mode.' 'No internet connection found. App is running in offline mode.',
); );
}); });
} }

View File

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