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

Application mgt new

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

@ -1,25 +1,17 @@
{ {
"name": "store", "name": "entgra",
"version": "1.0.0", "version": "1.0.0",
"description": "WSO2 IoT Server App Store", "description": "Entgra device management",
"main": "App.js", "main": "App.js",
"proxy": "http://localhost:3001",
"repository": {
"type": "git",
"url": "git://github.com/wso2/carbon-devicemgt"
},
"license": "Apache License 2.0", "license": "Apache License 2.0",
"dependencies": { "dependencies": {
"acorn": "^6.2.0", "acorn": "^6.2.0",
"antd": "^3.20.1", "antd": "^3.22.0",
"axios": "^0.18.1", "axios": "^0.18.1",
"d3": "^5.9.7",
"dagre": "^0.8.4",
"javascript-time-ago": "^2.0.1", "javascript-time-ago": "^2.0.1",
"keymirror": "^0.1.1", "keymirror": "^0.1.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"rc-viewer": "0.0.9", "rc-viewer": "0.0.9",
"react-d3-graph": "^2.1.0",
"react-highlight-words": "^0.16.0", "react-highlight-words": "^0.16.0",
"react-image-viewer-zoom": "^1.0.36", "react-image-viewer-zoom": "^1.0.36",
"react-infinite-scroller": "^1.2.4", "react-infinite-scroller": "^1.2.4",

@ -6,21 +6,21 @@
"primaryColor": "rgb(24, 144, 255)" "primaryColor": "rgb(24, 144, 255)"
}, },
"serverConfig": { "serverConfig": {
"invokerUri": "/ui-request-handler/invoke/application-mgt-store/v1.0", "invokerUri": "/ui-request-handler/invoke/application-mgt-entgra/v1.0",
"invoker": { "invoker": {
"uri": "/store-ui-request-handler/invoke", "uri": "/entgra-ui-request-handler/invoke",
"publisher": "/application-mgt-publisher/v1.0", "publisher": "/application-mgt-publisher/v1.0",
"store": "/application-mgt-store/v1.0", "entgra": "/application-mgt-entgra/v1.0",
"admin" : "", "admin" : "",
"deviceMgt" : "/device-mgt/v1.0" "deviceMgt" : "/device-mgt/v1.0"
}, },
"loginUri": "/store-ui-request-handler/login", "loginUri": "/entgra-ui-request-handler/login",
"logoutUri": "/store-ui-request-handler/logout", "logoutUri": "/entgra-ui-request-handler/logout",
"platform": "store" "platform": "entgra"
}, },
"defaultPlatformIcons": { "defaultPlatformIcons": {
"default": { "default": {
"icon": "global", "icon": "hdd",
"color": "#535c68", "color": "#535c68",
"theme": "outlined" "theme": "outlined"
}, },

@ -65,7 +65,7 @@ class App extends React.Component {
componentDidMount() { componentDidMount() {
axios.get( axios.get(
window.location.origin + "/store/public/conf/config.json", window.location.origin + "/entgra/public/conf/config.json",
).then(res => { ).then(res => {
console.log(res); console.log(res);
this.setState({ this.setState({
@ -88,7 +88,7 @@ class App extends React.Component {
<ConfigContext.Provider value={this.state.config}> <ConfigContext.Provider value={this.state.config}>
<div> <div>
<Switch> <Switch>
<Redirect exact from="/store" to="/store/android"/> <Redirect exact from="/entgra" to="/entgra/devices"/>
{this.props.routes.map((route) => ( {this.props.routes.map((route) => (
<RouteWithSubRoutes key={route.path} {...route} /> <RouteWithSubRoutes key={route.path} {...route} />
))} ))}

@ -18,26 +18,45 @@
import React from "react"; import React from "react";
import axios from "axios"; import axios from "axios";
import {Button, message, notification, Table, Typography} from "antd"; import {Tag, message, notification, Table, Typography, Tooltip, Icon, Divider} from "antd";
import TimeAgo from 'javascript-time-ago' import TimeAgo from 'javascript-time-ago'
// Load locale-specific relative date/time formatting rules. // Load locale-specific relative date/time formatting rules.
import en from 'javascript-time-ago/locale/en' import en from 'javascript-time-ago/locale/en'
import {withConfigContext} from "../../../../context/ConfigContext"; import {withConfigContext} from "../../context/ConfigContext";
const {Text} = Typography; const {Text} = Typography;
let config = null;
const columns = [ const columns = [
{ {
title: 'Device', title: 'Device',
dataIndex: 'name', dataIndex: 'name',
fixed: 'left',
width: 100, width: 100,
}, },
{ {
title: 'Modal', title: 'Type',
dataIndex: 'deviceInfo', dataIndex: 'type',
key: 'modal', key: 'type',
render: deviceInfo => `${deviceInfo.vendor} ${deviceInfo.deviceModel}` render: type => {
const defaultPlatformIcons = config.defaultPlatformIcons;
let icon = defaultPlatformIcons.default.icon;
let color = defaultPlatformIcons.default.color;
let theme = defaultPlatformIcons.default.theme;
if (defaultPlatformIcons.hasOwnProperty(type)) {
icon = defaultPlatformIcons[type].icon;
color = defaultPlatformIcons[type].color;
theme = defaultPlatformIcons[type].theme;
}
return (
<span style={{fontSize: 20, color: color, textAlign: "center"}}>
<Icon type={icon} theme={theme}/>
</span>
);
}
// todo add filtering options // todo add filtering options
}, },
{ {
@ -48,49 +67,61 @@ const columns = [
// todo add filtering options // todo add filtering options
}, },
{ {
title: 'Last Updated', title: 'Ownership',
dataIndex: 'enrolmentInfo', dataIndex: 'enrolmentInfo',
key: 'dateOfLastUpdate', key: 'ownership',
render: (data) => { render: enrolmentInfo => enrolmentInfo.ownership
return (getTimeAgo(data.dateOfLastUpdate));
}
// todo add filtering options // todo add filtering options
}, },
{ {
title: 'Status', title: 'Status',
dataIndex: 'enrolmentInfo', dataIndex: 'enrolmentInfo',
key: 'status', key: 'status',
render: enrolmentInfo => enrolmentInfo.status render: (enrolmentInfo) => {
const status = enrolmentInfo.status.toLowerCase();
let color = "#f9ca24";
switch (status) {
case "active":
color = "#badc58";
break;
case "created":
color = "#6ab04c";
break;
case "removed":
color = "#ff7979";
break;
case "inactive":
color = "#f9ca24";
break;
case "blocked":
color = "#636e72";
break;
}
return <Tag color={color}>{status}</Tag>;
}
// todo add filtering options // todo add filtering options
}, },
{ {
title: 'Ownership', title: 'Last Updated',
dataIndex: 'enrolmentInfo', dataIndex: 'enrolmentInfo',
key: 'ownership', key: 'dateOfLastUpdate',
render: enrolmentInfo => enrolmentInfo.ownership render: (data) => {
const {dateOfLastUpdate} = data;
const timeAgoString = getTimeAgo(dateOfLastUpdate);
return <Tooltip title={new Date(dateOfLastUpdate).toString()}>{timeAgoString}</Tooltip>;
}
// todo add filtering options // todo add filtering options
}, },
{ {
title: 'OS Version', title: 'Action',
dataIndex: 'deviceInfo', key: 'action',
key: 'osVersion', render: () => (
render: deviceInfo => deviceInfo.osVersion <span>
// todo add filtering options <a><Icon type="edit" /></a>
}, <Divider type="vertical" />
{ <a><Text type="danger"><Icon type="delete" /></Text></a>
title: 'IMEI', </span>
dataIndex: 'properties', ),
key: 'imei',
render: properties => {
let imei = "not-found";
for (let i = 0; i < properties.length; i++) {
if (properties[i].name === "IMEI") {
imei = properties[i].value;
}
}
return imei;
}
// todo add filtering options
}, },
]; ];
@ -100,9 +131,10 @@ const getTimeAgo = (time) => {
}; };
class DeviceInstall extends React.Component { class DeviceTable extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
config = this.props.context;
TimeAgo.addLocale(en); TimeAgo.addLocale(en);
this.state = { this.state = {
data: [], data: [],
@ -118,11 +150,7 @@ class DeviceInstall extends React.Component {
this.setState({ this.setState({
selectedRows: selectedRows selectedRows: selectedRows
}) })
}, }
getCheckboxProps: record => ({
disabled: record.name === 'Disabled User', // Column configuration not to be checked
name: record.name,
}),
}; };
componentDidMount() { componentDidMount() {
@ -133,19 +161,15 @@ class DeviceInstall extends React.Component {
fetch = (params = {}) => { fetch = (params = {}) => {
const config = this.props.context; const config = this.props.context;
this.setState({loading: true}); this.setState({loading: true});
const {deviceType} = this.props;
// get current page // get current page
const currentPage = (params.hasOwnProperty("page")) ? params.page : 1; const currentPage = (params.hasOwnProperty("page")) ? params.page : 1;
const extraParams = { const extraParams = {
offset: 10 * (currentPage - 1), //calculate the offset offset: 10 * (currentPage - 1), //calculate the offset
limit: 10, limit: 10,
status: "ACTIVE",
requireDeviceInfo: true, requireDeviceInfo: true,
type: deviceType
}; };
// note: encode with '%26' not '&'
const encodedExtraParams = Object.keys(extraParams).map(key => key + '=' + extraParams[key]).join('&'); const encodedExtraParams = Object.keys(extraParams).map(key => key + '=' + extraParams[key]).join('&');
//send request to the invoker //send request to the invoker
@ -160,15 +184,13 @@ class DeviceInstall extends React.Component {
data: res.data.data.devices, data: res.data.data.devices,
pagination, pagination,
}); });
} }
}).catch((error) => { }).catch((error) => {
console.log(error); if (error.hasOwnProperty("response") && error.response.status === 401) {
if (error.hasOwnProperty("status") && error.response.status === 401) {
//todo display a popop with error //todo display a popop with error
message.error('You are not logged in'); message.error('You are not logged in');
window.location.href = window.location.origin + '/store/login'; window.location.href = window.location.origin + '/entgra/login';
} else { } else {
notification["error"]({ notification["error"]({
message: "There was a problem", message: "There was a problem",
@ -197,28 +219,11 @@ class DeviceInstall extends React.Component {
}); });
}; };
install = () => {
const {selectedRows} = this.state;
const payload = [];
selectedRows.map(device => {
payload.push({
id: device.deviceIdentifier,
type: device.type
});
});
this.props.onInstall("devices", payload);
};
render() { render() {
const {data, pagination, loading, selectedRows} = this.state; const {data, pagination, loading, selectedRows} = this.state;
return ( return (
<div> <div>
<Text>
Start installing the application for one or more users by entering the corresponding user name.
Select install to automatically start downloading the application for the respective user/users.
</Text>
<Table <Table
style={{paddingTop: 20}}
columns={columns} columns={columns}
rowKey={record => record.deviceIdentifier} rowKey={record => record.deviceIdentifier}
dataSource={data} dataSource={data}
@ -234,15 +239,9 @@ class DeviceInstall extends React.Component {
rowSelection={this.rowSelection} rowSelection={this.rowSelection}
scroll={{x: 1000}} scroll={{x: 1000}}
/> />
<div style={{paddingTop: 10, textAlign: "right"}}>
<Button disabled={selectedRows.length === 0} htmlType="button" type="primary"
onClick={this.install}>
Install
</Button>
</div>
</div> </div>
); );
} }
} }
export default withConfigContext(DeviceInstall); export default withConfigContext(DeviceTable);

@ -1,78 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {Card, Typography, Col, Row} from 'antd';
import React from "react";
import {Link} from "react-router-dom";
import "../../App.css";
import StarRatings from 'react-star-ratings';
const {Meta} = Card;
const {Text} = Typography;
class AppCard extends React.Component {
constructor(props) {
super(props);
this.handleReleasesClick = this.handleReleasesClick.bind(this);
}
handleReleasesClick() {
this.props.openReleasesModal(this.props.app);
}
render() {
const app = this.props.app;
const release = this.props.app.applicationReleases[0];
const description = (
<div className="appCard">
<Link to={"/store/"+app.deviceType+"/apps/" + release.uuid}>
<Row className="release">
<Col span={24} className="release-icon">
<img src={release.iconPath} alt="icon"/>
{/*<Avatar shape="square" size={128} src={release.iconPath} />*/}
</Col>
<Col span={24}>
<Text strong level={4}>{app.name}</Text><br/>
<Text type="secondary" level={4}>{app.deviceType}</Text><br/>
<StarRatings
rating={app.rating}
starRatedColor="#777"
starDimension = "12px"
starSpacing = "0"
numberOfStars={5}
name='rating'
/>
</Col>
</Row>
</Link>
</div>
);
return (
<Card style={{marginTop: 16}}>
<Meta
description={description}
/>
</Card>
);
}
}
export default AppCard;

@ -1,120 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import AppCard from "./AppCard";
import {Col, message, notification, Row, Result, Skeleton} from "antd";
import axios from "axios";
import {withConfigContext} from "../../context/ConfigContext";
class AppList extends React.Component {
constructor(props) {
super(props);
this.state = {
apps: [],
loading: true
}
}
componentDidMount() {
const {deviceType} = this.props;
this.props.changeSelectedMenuItem(deviceType);
this.fetchData(deviceType);
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.deviceType !== this.props.deviceType) {
const {deviceType} = this.props;
this.props.changeSelectedMenuItem(deviceType);
this.fetchData(deviceType);
}
}
fetchData = (deviceType) => {
const config = this.props.context;
const payload = {};
if (deviceType === "web-clip") {
payload.appType = "WEB_CLIP";
} else {
payload.deviceType = deviceType;
}
this.setState({
loading: true
});
//send request to the invoker
axios.post(
window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.store + "/applications/",
payload,
).then(res => {
if (res.status === 200) {
//todo remove this property check after backend improvement
let apps = (res.data.data.hasOwnProperty("applications")) ? res.data.data.applications : [];
this.setState({
apps: apps,
loading: false
})
}
}).catch((error) => {
console.log(error.response);
if (error.hasOwnProperty("response") && error.response.status === 401) {
//todo display a popup with error
message.error('You are not logged in');
window.location.href = window.location.origin+ '/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load apps.",
});
}
this.setState({loading: false});
});
};
render() {
const {apps,loading} = this.state;
return (
<Skeleton loading={loading} active>
<Row gutter={16}>
{apps.length === 0 && (
<Result
status="404"
title="No apps, yet."
subTitle="No apps available, yet! When the administration uploads, apps will show up here."
// extra={<Button type="primary">Back Home</Button>}
/>
)}
{apps.map(app => (
<Col key={app.id} xs={12} sm={6} md={6} lg={4} xl={3}>
<AppCard key={app.id}
app={app}
/>
</Col>
))}
</Row>
</Skeleton>
);
}
}
export default withConfigContext(AppList);

@ -1,90 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.d-rating .numeric-data{
box-sizing: border-box;
display: inline-block;
padding: 20px 0 20px 0;
vertical-align: top;
text-align: center;
width: 30%;
}
.d-rating .bar-containers{
box-sizing: border-box;
display: inline-block;
padding: 20px 20px 20px 30px;
vertical-align: top;
width: 70%;
}
.d-rating .bar-containers .bar-container{
color: #737373;
font-weight: 400;
height: 20px;
margin-bottom: 4px;
position: relative;
width: 100%;
}
.d-rating .bar-containers .bar-container .number{
font-size: 11px;
left: -16px;
letter-spacing: 1px;
position: absolute;
}
.d-rating .bar-containers .bar-container .bar{
transition: width .25s ease;
display: inline-block;
height: 100%;
opacity: .8;
border-radius: 5px;
}
.bar-container .rate-5{
background: #57bb8a;
}
.bar-container .rate-4{
background: #9ace6a;
}
.bar-container .rate-3{
background: #ffcf02;
}
.bar-container .rate-2{
background: #ff9f02;
}
.bar-container .rate-1{
background: #ff6f31;
}
.d-rating .numeric-data .rate{
color: #333;
font-size: 64px;
font-weight: 100;
line-height: 64px;
padding-bottom: 6px;
}
.d-rating .numeric-data .people-count{
padding-top: 6px;
}

@ -1,140 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Row, Typography, Icon} from "antd";
import StarRatings from "react-star-ratings";
import "./DetailedRating.css";
import axios from "axios";
import {withConfigContext} from "../../../context/ConfigContext";
const { Text } = Typography;
class DetailedRating extends React.Component{
constructor(props){
super(props);
this.state={
detailedRating: null
}
}
componentDidMount() {
const {type,uuid} = this.props;
this.getData(type,uuid);
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.uuid !== this.props.uuid) {
const {type,uuid} = this.props;
this.getData(type,uuid);
}
}
getData = (type, uuid)=>{
const config = this.props.context;
return axios.get(
window.location.origin+ config.serverConfig.invoker.uri +config.serverConfig.invoker.store+"/reviews/"+uuid+"/"+type+"-rating",
).then(res => {
if (res.status === 200) {
let detailedRating = res.data.data;
this.setState({
detailedRating
})
}
}).catch(function (error) {
if (error.response.status === 401) {
window.location.href = window.location.origin+'/store/login';
}
});
};
render() {
const detailedRating = this.state.detailedRating;
if(detailedRating ==null){
return null;
}
const totalCount = detailedRating.noOfUsers;
const ratingVariety = detailedRating.ratingVariety;
const ratingArray = [];
for (let [key, value] of Object.entries(ratingVariety)) {
ratingArray.push(value);
}
const maximumRating = Math.max(...ratingArray);
const ratingBarPercentages = [0,0,0,0,0];
if(maximumRating>0){
for(let i = 0; i<5; i++){
ratingBarPercentages[i] = (ratingVariety[(i+1).toString()])/maximumRating*100;
}
}
return (
<Row className="d-rating">
<div className="numeric-data">
<div className="rate">{detailedRating.ratingValue.toFixed(1)}</div>
<StarRatings
rating={detailedRating.ratingValue}
starRatedColor="#777"
starDimension = "16px"
starSpacing = "2px"
numberOfStars={5}
name='rating'
/>
<br/>
<Text type="secondary" className="people-count"><Icon type="team" /> {totalCount} total</Text>
</div>
<div className="bar-containers">
<div className="bar-container">
<span className="number">5</span>
<span className="bar rate-5" style={{width: ratingBarPercentages[4]+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">4</span>
<span className="bar rate-4" style={{width: ratingBarPercentages[3]+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">3</span>
<span className="bar rate-3" style={{width: ratingBarPercentages[2]+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">2</span>
<span className="bar rate-2" style={{width: ratingBarPercentages[1]+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">1</span>
<span className="bar rate-1" style={{width: ratingBarPercentages[0]+"%"}}> </span>
</div>
</div>
</Row>
);
}
}
export default withConfigContext(DetailedRating);

@ -1,171 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Divider, Row, Col, Typography, Button, Rate, notification} from "antd";
import "../../../App.css";
import ImgViewer from "../../apps/release/images/ImgViewer";
import StarRatings from "react-star-ratings";
import DetailedRating from "./DetailedRating";
import Reviews from "./review/Reviews";
import axios from "axios";
import AppInstallModal from "./install/AppInstallModal";
import CurrentUsersReview from "./review/CurrentUsersReview";
import {withConfigContext} from "../../../context/ConfigContext";
const {Title, Text, Paragraph} = Typography;
class ReleaseView extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
appInstallModalVisible: false
}
}
installApp = (type, payload) => {
const config = this.props.context;
const release = this.props.app.applicationReleases[0];
const {uuid} = release;
this.setState({
loading: true,
});
const url = window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.store + "/subscription/" + uuid + "/" + type + "/install";
axios.post(
url,
payload,
{
headers: {'X-Platform': config.serverConfig.platform}
}
).then(res => {
if (res.status === 200) {
this.setState({
loading: false,
appInstallModalVisible: false
});
notification["success"]({
message: 'Done!',
description:
'App installed triggered.',
});
} else {
this.setState({
loading: false
});
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while installing app",
});
}
}).catch((error) => {
if (error.response.status === 401) {
window.location.href = window.location.origin+ '/store/login';
} else {
this.setState({
loading: false,
visible: false
});
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while installing the app.",
});
}
});
};
showAppInstallModal = () => {
this.setState({
appInstallModalVisible: true
});
};
closeAppInstallModal = () => {
this.setState({
appInstallModalVisible: false
});
};
render() {
const {app,deviceType} = this.props;
const release = app.applicationReleases[0];
return (
<div>
<AppInstallModal
uuid={release.uuid}
visible={this.state.appInstallModalVisible}
deviceType = {deviceType}
onClose={this.closeAppInstallModal}
onInstall={this.installApp}/>
<div className="release">
<Row>
<Col xl={4} sm={6} xs={8} className="release-icon">
<img src={release.iconPath} alt="icon"/>
</Col>
<Col xl={10} sm={11} className="release-title">
<Title level={2}>{app.name}</Title>
<Text>Version : {release.version}</Text><br/><br/>
<StarRatings
rating={app.rating}
starRatedColor="#777"
starDimension="20px"
starSpacing="2px"
numberOfStars={5}
name='rating'
/>
</Col>
<Col xl={8} md={10} sm={24} xs={24} style={{float: "right"}}>
<div>
<Button.Group style={{float: "right"}}>
<Button onClick={this.showAppInstallModal} loading={this.state.loading}
htmlType="button" type="primary" icon="download">Install</Button>
</Button.Group>
</div>
</Col>
</Row>
<Divider/>
<Row>
<ImgViewer images={release.screenshots}/>
</Row>
<Divider/>
<Paragraph type="secondary" ellipsis={{rows: 3, expandable: true}}>
{release.description}
</Paragraph>
<Divider/>
<CurrentUsersReview uuid={release.uuid}/>
<Divider dashed={true}/>
<Text>REVIEWS</Text>
<Row>
<Col lg={18} md={24}>
<DetailedRating type="app" uuid={release.uuid}/>
</Col>
</Row>
<Reviews type="app" uuid={release.uuid}/>
</div>
</div>
);
}
}
export default withConfigContext(ReleaseView);

@ -1,63 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, {Component} from 'react';
import RcViewer from 'rc-viewer';
import {Col} from "antd";
class ImgViewer extends Component {
render() {
const options = {
title: false,
toolbar: {
zoomIn: 0,
zoomOut: 0,
oneToOne: 0,
reset: 0,
prev: 1,
play: {
show: 0
},
next: 1,
rotateLeft: 0,
rotateRight: 0,
flipHorizontal: 0,
flipVertical: 0
},
rotatable: false,
transition: false,
movable : false
};
return (
<div>
<RcViewer options={options} ref='viewer'>
{this.props.images.map((screenshotUrl) => {
return (
<Col key={"col-" + screenshotUrl} lg={6} md={8} xs={8} className="release-screenshot">
<img key={screenshotUrl} src={screenshotUrl}/>
</Col>
)
})}
</RcViewer>
</div>
);
}
}
export default ImgViewer;

@ -1,62 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Modal, Tabs} from "antd";
import UserInstall from "./UserInstall";
import GroupInstall from "./GroupInstall";
import RoleInstall from "./RoleInstall";
import DeviceInstall from "./DeviceInstall";
const { TabPane } = Tabs;
class AppInstallModal extends React.Component{
state={
data:[]
};
render() {
const {deviceType} = this.props;
return (
<div>
<Modal
title="Install App"
visible={this.props.visible}
onCancel={this.props.onClose}
footer={null}
>
<Tabs defaultActiveKey="device">
<TabPane tab="Device" key="device">
<DeviceInstall deviceType={deviceType} onInstall={this.props.onInstall}/>
</TabPane>
<TabPane tab="User" key="user">
<UserInstall onInstall={this.props.onInstall}/>
</TabPane>
<TabPane tab="Role" key="role">
<RoleInstall onInstall={this.props.onInstall}/>
</TabPane>
<TabPane tab="Group" key="group">
<GroupInstall onInstall={this.props.onInstall}/>
</TabPane>
</Tabs>
</Modal>
</div>
);
}
}
export default AppInstallModal;

@ -1,133 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Typography, Select, Spin, message, notification, Button} from "antd";
import debounce from 'lodash.debounce';
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
const {Text} = Typography;
const {Option} = Select;
class GroupInstall extends React.Component {
constructor(props) {
super(props);
this.lastFetchId = 0;
this.fetchUser = debounce(this.fetchUser, 800);
}
state = {
data: [],
value: [],
fetching: false,
};
fetchUser = value => {
this.lastFetchId += 1;
const fetchId = this.lastFetchId;
const config = this.props.context;
this.setState({data: [], fetching: true});
axios.post(
window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt+"/groups?name=" + value,
).then(res => {
if (res.status === 200) {
if (fetchId !== this.lastFetchId) {
// for fetch callback order
return;
}
const data = res.data.data.deviceGroups.map(group => ({
text: group.name,
value: group.name,
}));
this.setState({data, fetching: false});
}
}).catch((error) => { console.log(error);
if (error.hasOwnProperty("status") && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin+'/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load groups.",
});
}
this.setState({fetching: false});
});
};
handleChange = value => {
this.setState({
value,
data: [],
fetching: false,
});
};
install = () =>{
const {value} = this.state;
const data = [];
value.map(val=>{
data.push(val.key);
});
this.props.onInstall("group",data);
};
render() {
const {fetching, data, value} = this.state;
return (
<div>
<Text>Start installing the application for one or more groups by entering the corresponding group name. Select install to automatically start downloading the application for the respective device group/ groups.</Text>
<br/>
<br/>
<Select
mode="multiple"
labelInValue
value={value}
placeholder="Search groups"
notFoundContent={fetching ? <Spin size="small"/> : null}
filterOption={false}
onSearch={this.fetchUser}
onChange={this.handleChange}
style={{width: '100%'}}
>
{data.map(d => (
<Option key={d.value}>{d.text}</Option>
))}
</Select>
<div style={{paddingTop:10, textAlign:"right"}}>
<Button disabled={value.length===0} htmlType="button" type="primary" onClick={this.install}>Install</Button>
</div>
</div>
);
}
}
export default withConfigContext(GroupInstall);

@ -1,133 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Typography, Select, Spin, message, notification, Button} from "antd";
import debounce from 'lodash.debounce';
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
const {Text} = Typography;
const {Option} = Select;
class RoleInstall extends React.Component {
constructor(props) {
super(props);
this.lastFetchId = 0;
this.fetchUser = debounce(this.fetchUser, 800);
}
state = {
data: [],
value: [],
fetching: false,
};
fetchUser = value => {
const config = this.props.context;
this.lastFetchId += 1;
const fetchId = this.lastFetchId;
this.setState({data: [], fetching: true});
axios.get(
window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt+"/roles?filter=" + value,
).then(res => {
if (res.status === 200) {
if (fetchId !== this.lastFetchId) {
// for fetch callback order
return;
}
const data = res.data.data.roles.map(role => ({
text: role,
value: role,
}));
this.setState({data, fetching: false});
}
}).catch((error) => { console.log(error);
if (error.hasOwnProperty("status") && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin+'/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load roles.",
});
}
this.setState({fetching: false});
});
};
handleChange = value => {
this.setState({
value,
data: [],
fetching: false,
});
};
install = () =>{
const {value} = this.state;
const data = [];
value.map(val=>{
data.push(val.key);
});
this.props.onInstall("role",data);
};
render() {
const {fetching, data, value} = this.state;
return (
<div>
<Text>Start installing the application for one or more roles by entering the corresponding role name. Select install to automatically start downloading the application for the respective user role/roles.</Text>
<br/>
<br/>
<Select
mode="multiple"
labelInValue
value={value}
placeholder="Search roles"
notFoundContent={fetching ? <Spin size="small"/> : null}
filterOption={false}
onSearch={this.fetchUser}
onChange={this.handleChange}
style={{width: '100%'}}
>
{data.map(d => (
<Option key={d.value}>{d.text}</Option>
))}
</Select>
<div style={{paddingTop:10, textAlign:"right"}}>
<Button disabled={value.length===0} htmlType="button" type="primary" onClick={this.install}>Install</Button>
</div>
</div>
);
}
}
export default withConfigContext(RoleInstall);

@ -1,133 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Typography, Select, Spin, message, notification, Button} from "antd";
import debounce from 'lodash.debounce';
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
const {Text} = Typography;
const {Option} = Select;
class UserInstall extends React.Component {
constructor(props) {
super(props);
this.lastFetchId = 0;
this.fetchUser = debounce(this.fetchUser, 800);
}
state = {
data: [],
value: [],
fetching: false,
};
fetchUser = value => {
const config = this.props.context;
this.lastFetchId += 1;
const fetchId = this.lastFetchId;
this.setState({data: [], fetching: true});
//send request to the invoker
axios.get(
window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt+"/users/search?username=" + value,
).then(res => {
if (res.status === 200) {
if (fetchId !== this.lastFetchId) {
// for fetch callback order
return;
}
const data = res.data.data.users.map(user => ({
text: user.username,
value: user.username,
}));
this.setState({data, fetching: false});
}
}).catch((error) => {
if (error.response.hasOwnProperty(status) && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin+ '/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load users.",
});
}
this.setState({fetching: false});
});
};
handleChange = value => {
this.setState({
value,
data: [],
fetching: false,
});
};
install = () => {
const {value} = this.state;
const data = [];
value.map(val => {
data.push(val.key);
});
this.props.onInstall("user", data);
};
render() {
const {fetching, data, value} = this.state;
return (
<div>
<Text>Start installing the application for one or more users by entering the corresponding user name. Select install to automatically start downloading the application for the respective user/users. </Text>
<p>Select users</p>
<Select
mode="multiple"
labelInValue
value={value}
placeholder="Enter the username"
notFoundContent={fetching ? <Spin size="small"/> : null}
filterOption={false}
onSearch={this.fetchUser}
onChange={this.handleChange}
style={{width: '100%'}}
>
{data.map(d => (
<Option key={d.value}>{d.text}</Option>
))}
</Select>
<div style={{paddingTop: 10, textAlign: "right"}}>
<Button disabled={value.length===0} htmlType="button" type="primary" onClick={this.install}>Install</Button>
</div>
</div>
);
}
}
export default withConfigContext(UserInstall);

@ -1,181 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Drawer, Button, Icon, Row, Col, Typography, Divider, Input, Spin, notification} from 'antd';
import StarRatings from "react-star-ratings";
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
const {Title} = Typography;
const {TextArea} = Input;
class AddReview extends React.Component {
state = {
visible: false,
content: '',
rating: 0,
loading: false
};
showDrawer = () => {
this.setState({
visible: true,
content: '',
rating: 0,
loading: false
});
};
onClose = () => {
this.setState({
visible: false,
});
};
changeRating = (newRating, name) => {
this.setState({
rating: newRating
});
};
onChange = (e) => {
this.setState({content: e.target.value})
};
onSubmit = () => {
const config = this.props.context;
const {content, rating} = this.state;
const {uuid} = this.props;
this.setState({
loading: true
});
const payload = {
content: content,
rating: rating
};
axios.post(
window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.store + "/reviews/" + uuid,
payload,
).then(res => {
if (res.status === 201) {
this.setState({
loading: false,
visible: false
});
notification["success"]({
message: 'Done!',
description:
'Your review has been posted successfully.',
});
setTimeout(() => {
window.location.href = uuid;
}, 2000)
} else {
this.setState({
loading: false,
visible: false
});
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"We are unable to add your review right now.",
});
}
}).catch((error) => {
if (error.response.status === 401) {
window.location.href = window.location.origin+ '/store/login';
} else {
this.setState({
loading: false,
visible: false
});
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"We are unable to add your review right now.",
});
}
});
};
render() {
return (
<div>
<Button type="primary" onClick={this.showDrawer}>
<Icon type="star"/> Add a review
</Button>
<Drawer
// title="Basic Drawer"
placement="bottom"
closable={false}
onClose={this.onClose}
visible={this.state.visible}
height={400}
>
<Spin spinning={this.state.loading} tip="Posting your review...">
<Row>
<Col lg={8}/>
<Col lg={8}>
<Title level={4}>Add review</Title>
<Divider/>
<TextArea
placeholder="Tell others what you think about this app. Would you recommend it, and why?"
onChange={this.onChange}
rows={4}
value={this.state.content || ''}
style={{marginBottom: 20}}
/>
<StarRatings
rating={this.state.rating}
changeRating={this.changeRating}
starRatedColor="#777"
starHoverColor="#444"
starDimension="20px"
starSpacing="2px"
numberOfStars={5}
name='rating'
/>
<br/><br/>
<Button onClick={this.onClose} style={{marginRight: 8}}>
Cancel
</Button>
<Button disabled={this.state.rating === 0} onClick={this.onSubmit} type="primary">
Submit
</Button>
</Col>
</Row>
</Spin>
</Drawer>
</div>
);
}
}
export default withConfigContext(AddReview);

@ -1,128 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {List, message, Typography, Empty, Button, Row, Col, notification} from "antd";
import SingleReview from "./singleReview/SingleReview";
import axios from "axios";
import AddReview from "./AddReview";
import {withConfigContext} from "../../../../context/ConfigContext";
const {Text, Paragraph} = Typography;
class CurrentUsersReview extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
componentDidMount() {
this.fetchData();
}
fetchData = () => {
const {uuid} = this.props;
const config = this.props.context;
axios.get(
window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.store + "/reviews/app/user/" + uuid,
).then(res => {
if (res.status === 200) {
const data = res.data.data.data;
this.setState({data});
}
}).catch((error) => {
if (error.response.hasOwnProperty(status) && error.response.status === 401) {
message.error('You are not logged in');
window.location.href = window.location.origin+ '/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to get your review.",
});
}
});
};
deleteCallback = () =>{
this.setState({
data: []
});
};
addCallBack =(review) =>{
};
render() {
const {data} = this.state;
const {uuid} = this.props;
return (
<div>
<Text>MY REVIEW</Text>
<div style={{
overflow: "auto",
paddingTop: 8,
paddingLeft: 24
}}>
{data.length > 0 && (
<div>
<List
dataSource={data}
renderItem={item => (
<List.Item key={item.id}>
<SingleReview uuid={uuid} review={item} isDeletable={true} isEditable={true} deleteCallback={this.deleteCallback} isPersonalReview={true}/>
</List.Item>
)}
>
</List>
</div>
)}
{data.length === 0 && (
<div>
<Empty
image={Empty.PRESENTED_IMAGE_DEFAULT}
imagestyle={{
height: 60,
}}
description={
<span>Share your experience with your community by adding a review.</span>
}
>
{/*<Button type="primary">Add review</Button>*/}
<AddReview uuid={uuid}/>
</Empty>
</div>
)}
</div>
</div>
);
}
}
export default withConfigContext(CurrentUsersReview);

@ -1,34 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.infinite-container {
overflow: auto;
padding: 8px 24px;
}
.loading-container {
position: absolute;
bottom: 40px;
width: 100%;
text-align: center;
}
.loading {
position: absolute;
bottom: -40px;
left: 50%;
}

@ -1,155 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {List, message, Avatar, Spin, Button, notification} from 'antd';
import "./Reviews.css";
import InfiniteScroll from 'react-infinite-scroller';
import SingleReview from "./singleReview/SingleReview";
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
const limit = 5;
class Reviews extends React.Component {
state = {
data: [],
loading: false,
hasMore: false,
loadMore: false
};
componentDidMount() {
this.fetchData(0, limit, res => {
this.setState({
data: res,
});
});
}
fetchData = (offset, limit, callback) => {
const {uuid, type} = this.props;
const config = this.props.context;
axios.get(
window.location.origin+ config.serverConfig.invoker.uri +config.serverConfig.invoker.store+"/reviews/"+type+"/"+uuid,
{
headers: {'X-Platform': config.serverConfig.platform}
}).then(res => {
if (res.status === 200) {
let reviews = res.data.data.data;
callback(reviews);
}
}).catch(function (error) {
if (error.response.status === 401) {
window.location.href = window.location.origin+ '/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load reviews.",
});
}
});
};
handleInfiniteOnLoad = (count) => {
const offset = count * limit;
let data = this.state.data;
this.setState({
loading: true,
});
if (data.length > 149) {
this.setState({
hasMore: false,
loading: false,
});
return;
}
this.fetchData(offset, limit, res => {
if (res.length > 0) {
data = data.concat(res);
this.setState({
data,
loading: false,
});
} else {
this.setState({
hasMore: false,
loading: false
});
}
});
};
enableLoading = () => {
this.setState({
hasMore: true,
loadMore: true
});
};
deleteCallback = () =>{
this.fetchData(0, limit, res => {
this.setState({
data: res,
});
});
};
render() {
const {loading, hasMore, data, loadMore} = this.state;
const {uuid} = this.props;
return (
<div className="infinite-container">
<InfiniteScroll
initialLoad={false}
pageStart={0}
loadMore={this.handleInfiniteOnLoad}
hasMore={!loading && hasMore}
useWindow={true}
>
<List
dataSource={data}
renderItem={item => (
<List.Item key={item.id}>
<SingleReview uuid={uuid} review={item} isDeletable={true} isEditable={false} deleteCallback={this.deleteCallback}/>
</List.Item>
)}
>
{loading && hasMore && (
<div className="loading-container">
<Spin/>
</div>
)}
</List>
</InfiniteScroll>
{!loadMore && (data.length >= limit) && (<div style={{textAlign: "center"}}>
<Button type="dashed" htmlType="button" onClick={this.enableLoading}>Read All Reviews</Button>
</div>)}
</div>
);
}
}
export default withConfigContext(Reviews);

@ -1,44 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
img.twemoji {
height: 1em;
width: 1em;
margin: 0 .05em 0 .1em;
vertical-align: -0.1em;
}
.edit-button {
color: #1890ff;
text-decoration: none;
outline: none;
cursor: pointer;
font-weight: normal;
font-size: 0.8em;
padding-left: 5px;
}
.delete-button {
color: #e74c3c;
text-decoration: none;
outline: none;
cursor: pointer;
font-weight: normal;
font-size: 0.8em;
padding-left: 5px;
}

@ -1,152 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Avatar, notification} from "antd";
import {List, Typography, Popconfirm} from "antd";
import StarRatings from "react-star-ratings";
import Twemoji from "react-twemoji";
import "./SingleReview.css";
import EditReview from "./editReview/EditReview";
import axios from "axios";
import {withConfigContext} from "../../../../../context/ConfigContext";
const {Text, Paragraph} = Typography;
const colorList = ['#f0932b', '#badc58', '#6ab04c', '#eb4d4b', '#0abde3', '#9b59b6', '#3498db', '#22a6b3', '#e84393', '#f9ca24'];
class SingleReview extends React.Component {
static defaultProps = {
isPersonalReview: false
};
constructor(props) {
super(props);
const {username} = this.props.review;
const color = colorList[username.length % 10];
this.state = {
content: '',
rating: 0,
color: color,
review: props.review
}
}
updateCallback = (review) => {
this.setState({
review
});
};
deleteReview = () => {
const {uuid} = this.props;
const {id} = this.state.review;
const config = this.props.context;
let url =window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.store;
// call as an admin api if the review is not a personal review
if (!this.props.isPersonalReview) {
url += "/admin";
}
url += "/reviews/" + uuid + "/" + id;
axios.delete(url).then(res => {
if (res.status === 200) {
notification["success"]({
message: 'Done!',
description:
'The review has been deleted successfully.',
});
this.props.deleteCallback(id);
}
}).catch((error) => {
console.log(error);
if (error.hasOwnProperty("response") && error.response.status === 401) {
window.location.href = window.location.origin+ '/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"We were unable to delete the review..",
});
}
});
};
render() {
const {isEditable, isDeletable, uuid} = this.props;
const {color, review} = this.state;
const {content, rating, username} = review;
const avatarLetter = username.charAt(0).toUpperCase();
const body = (
<div style={{marginTop: -5}}>
<StarRatings
rating={rating}
starRatedColor="#777"
starDimension="12px"
starSpacing="2px"
numberOfStars={5}
name='rating'
/>
<Text style={{fontSize: 12, color: "#aaa"}} type="secondary"> {review.createdAt}</Text><br/>
<Paragraph style={{color: "#777"}}>
<Twemoji options={{className: 'twemoji'}}>
{content}
</Twemoji>
</Paragraph>
</div>
);
const title = (
<div>
{review.username}
{isEditable && (<EditReview uuid={uuid} review={review} updateCallback={this.updateCallback}/>)}
{isDeletable && (
<Popconfirm
title="Are you sure delete this review?"
onConfirm={this.deleteReview}
okText="Yes"
cancelText="No"
>
<span className="delete-button">delete</span>
</Popconfirm>)}
</div>
);
return (
<div>
<List.Item.Meta
avatar={
<Avatar style={{backgroundColor: color, verticalAlign: 'middle'}} size="large">
{avatarLetter}
</Avatar>
}
title={title}
description={body}
/>
</div>
);
}
}
export default withConfigContext(SingleReview);

@ -1,27 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.edit-button {
color: #1890ff;
text-decoration: none;
outline: none;
cursor: pointer;
font-weight: normal;
font-size: 0.8em;
padding-left: 5px;
}

@ -1,191 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Drawer, Button, Icon, Row, Col, Typography, Divider, Input, Spin, notification} from 'antd';
import StarRatings from "react-star-ratings";
import axios from "axios";
import "./EditReview.css";
import {withConfigContext} from "../../../../../../context/ConfigContext";
const {Title} = Typography;
const {TextArea} = Input;
class EditReview extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
content: '',
rating: 0,
loading: false
};
}
componentDidMount() {
const {content,rating,id} = this.props.review;
console.log(this.props.review);
this.setState({
content,
rating
});
}
showDrawer = () => {
this.setState({
visible: true,
loading: false
});
};
onClose = () => {
this.setState({
visible: false,
});
};
changeRating = (newRating, name) => {
this.setState({
rating: newRating
});
};
onChange = (e) => {
this.setState({content: e.target.value})
};
onSubmit = () => {
const config = this.props.context;
const {content, rating} = this.state;
const {id} = this.props.review;
const {uuid} = this.props;
this.setState({
loading: true
});
const payload = {
content: content,
rating: rating
};
axios.put(
window.location.origin+ config.serverConfig.invoker.uri + config.serverConfig.invoker.store + "/reviews/" + uuid+"/"+id,
payload,
).then(res => {
if (res.status === 200) {
this.setState({
loading: false,
visible: false
});
notification["success"]({
message: 'Done!',
description:
'Your review has been update successfully.',
});
this.props.updateCallback(res.data.data);
} else {
this.setState({
loading: false,
visible: false
});
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"We are unable to update your review right now.",
});
}
}).catch((error) => {
console.log(error);
if (error.hasOwnProperty("response") && error.response.status === 401) {
window.location.href = window.location.origin+ '/store/login';
} else {
this.setState({
loading: false,
visible: false
});
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"We are unable to add your review right now.",
});
}
});
};
render() {
return (
<span>
<span className="edit-button" onClick={this.showDrawer}>edit</span>
<Drawer
// title="Basic Drawer"
placement="bottom"
closable={false}
onClose={this.onClose}
visible={this.state.visible}
height={400}
>
<Spin spinning={this.state.loading} tip="Posting your review...">
<Row>
<Col lg={8}/>
<Col lg={8}>
<Title level={4}>Edit review</Title>
<Divider/>
<TextArea
placeholder="Tell others what you think about this app. Would you recommend it, and why?"
onChange={this.onChange}
rows={6}
value={this.state.content || ''}
style={{marginBottom: 20}}
/>
<StarRatings
rating={this.state.rating}
changeRating={this.changeRating}
starRatedColor="#777"
starHoverColor="#444"
starDimension="20px"
starSpacing="2px"
numberOfStars={5}
name='rating'
/>
<br/><br/>
<Button onClick={this.onClose} style={{marginRight: 8}}>
Cancel
</Button>
<Button disabled={this.state.rating === 0} onClick={this.onSubmit} type="primary">
Submit
</Button>
</Col>
</Row>
</Spin>
</Drawer>
</span>
);
}
}
export default withConfigContext(EditReview);

@ -20,7 +20,7 @@
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"/> <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"/>
<title>Entgra App Store</title> <title>Entgra Device Management</title>
</head> </head>
<div id="root"></div> <div id="root"></div>
</html> </html>

@ -21,31 +21,37 @@ 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 Release from "./pages/dashboard/apps/release/Release";
import './index.css'; import './index.css';
import Devices from "./pages/Dashboard/Devices/Devices";
import Reports from "./pages/Dashboard/Reports/Reports";
import Geo from "./pages/Dashboard/Geo/Geo";
const routes = [ const routes = [
{ {
path: '/store/login', path: '/entgra/login',
exact: true, exact: true,
component: Login component: Login
}, },
{ {
path: '/store', path: '/entgra',
exact: false, exact: false,
component: Dashboard, component: Dashboard,
routes: [ routes: [
{ {
path: '/store/:deviceType', path: '/entgra/devices',
component: Apps, component: Devices,
exact: true exact: true
}, },
{ {
path: '/store/:deviceType/apps/:uuid', path: '/entgra/geo',
exact: true, component: Geo,
component: Release exact: true
},
{
path: '/entgra/reports',
component: Reports,
exact: true
} }
] ]
} }

@ -0,0 +1,94 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Layout, Menu, Icon} from 'antd';
import {Switch, Link} from "react-router-dom";
import RouteWithSubRoutes from "../../components/RouteWithSubRoutes"
import {Redirect} from 'react-router'
import "../../App.css";
import {withConfigContext} from "../../context/ConfigContext";
import Logout from "./Logout/Logout";
const {Header, Content, Footer} = Layout;
const {SubMenu} = Menu;
class Dashboard extends React.Component {
constructor(props) {
super(props);
this.state = {
routes: props.routes,
selectedKeys: [],
deviceTypes: []
};
this.logo = this.props.context.theme.logo;
}
render() {
return (
<div>
<Layout className="layout">
<Header style={{paddingLeft: 0, paddingRight: 0}}>
<div className="logo-image">
<img alt="logo" src={this.logo}/>
</div>
<Menu
theme="light"
mode="horizontal"
defaultSelectedKeys={['1']}
style={{lineHeight: '64px'}}
>
<Menu.Item key="devices"><Link to="/entgra/devices"><Icon type="appstore"/>Devices</Link></Menu.Item>
<Menu.Item key="geo"><Link to="/entgra/geo"><Icon type="environment"/>Geo</Link></Menu.Item>
<Menu.Item key="reports"><Link to="/entgra/reports"><Icon type="bar-chart"/>Reports</Link></Menu.Item>
<SubMenu className="profile"
title={
<span className="submenu-title-wrapper">
<Icon type="user"/>
Profile
</span>
}
>
<Logout/>
</SubMenu>
</Menu>
</Header>
</Layout>
<Layout>
<Content style={{marginTop: 2}}>
<Switch>
<Redirect exact from="/entgra" to="/entgra/devices"/>
{this.state.routes.map((route) => (
<RouteWithSubRoutes key={route.path} {...route} />
))}
</Switch>
</Content>
<Footer style={{textAlign: 'center'}}>
©2019 entgra.io
</Footer>
</Layout>
</div>
);
}
}
export default withConfigContext(Dashboard);

@ -0,0 +1,67 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {
PageHeader,
Typography,
Breadcrumb,
Icon,
Card
} from "antd";
import {Link} from "react-router-dom";
import DeviceTable from "../../../components/Devices/DevicesTable";
const {Paragraph} = Typography;
class Devices extends React.Component {
routes;
constructor(props) {
super(props);
this.routes = props.routes;
}
render() {
return (
<div>
<PageHeader style={{paddingTop: 0}}>
<Breadcrumb style={{paddingBottom: 16}}>
<Breadcrumb.Item>
<Link to="/entgra/devices"><Icon type="home"/> Home</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Devices</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Devices</h3>
<Paragraph>Lorem ipsum dolor sit amet, est similique constituto at, quot inermis id mel, an
illud incorrupte nam.</Paragraph>
</div>
</PageHeader>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 720}}>
<div style={{backgroundColor:"#ffffff", borderRadius: 5}}>
<DeviceTable/>
</div>
</div>
</div>
);
}
}
export default Devices;

@ -0,0 +1,65 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {
PageHeader,
Typography,
Breadcrumb,
Icon,
Card
} from "antd";
import {Link} from "react-router-dom";
import DeviceTable from "../../../components/Devices/DevicesTable";
const {Paragraph} = Typography;
class Geo extends React.Component {
routes;
constructor(props) {
super(props);
this.routes = props.routes;
}
render() {
return (
<div>
<PageHeader style={{paddingTop: 0}}>
<Breadcrumb style={{paddingBottom: 16}}>
<Breadcrumb.Item>
<Link to="/entgra"><Icon type="home"/> Home</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Geo</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Geo</h3>
<Paragraph>Lorem ipsum dolor sit amet, est similique constituto at, quot inermis id mel, an
illud incorrupte nam.</Paragraph>
</div>
</PageHeader>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 720}}>
</div>
</div>
);
}
}
export default Geo;

@ -49,7 +49,7 @@ class Logout extends React.Component {
).then(res => { ).then(res => {
//if the api call status is correct then user will logout and then it goes to login page //if the api call status is correct then user will logout and then it goes to login page
if (res.status === 200) { if (res.status === 200) {
window.location = window.location.origin + "/store/login"; window.location = window.location.origin + "/entgra/login";
} }
}).catch(function (error) { }).catch(function (error) {

@ -0,0 +1,65 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {
PageHeader,
Typography,
Breadcrumb,
Icon,
Card
} from "antd";
import {Link} from "react-router-dom";
import DeviceTable from "../../../components/Devices/DevicesTable";
const {Paragraph} = Typography;
class Reports extends React.Component {
routes;
constructor(props) {
super(props);
this.routes = props.routes;
}
render() {
return (
<div>
<PageHeader style={{paddingTop: 0}}>
<Breadcrumb style={{paddingBottom: 16}}>
<Breadcrumb.Item>
<Link to="/entgra"><Icon type="home"/> Home</Link>
</Breadcrumb.Item>
<Breadcrumb.Item>Reports</Breadcrumb.Item>
</Breadcrumb>
<div className="wrap">
<h3>Reports</h3>
<Paragraph>Lorem ipsum dolor sit amet, est similique constituto at, quot inermis id mel, an
illud incorrupte nam.</Paragraph>
</div>
</PageHeader>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 720}}>
</div>
</div>
);
}
}
export default Reports;

@ -93,7 +93,7 @@ class NormalLoginForm extends React.Component {
const parameters = { const parameters = {
username: values.username, username: values.username,
password: values.password, password: values.password,
platform: "store" platform: "entgra"
}; };
const request = Object.keys(parameters).map(key => key + '=' + parameters[key]).join('&'); const request = Object.keys(parameters).map(key => key + '=' + parameters[key]).join('&');
@ -101,7 +101,7 @@ class NormalLoginForm extends React.Component {
axios.post(window.location.origin+ config.serverConfig.loginUri, request axios.post(window.location.origin+ config.serverConfig.loginUri, request
).then(res => { ).then(res => {
if (res.status === 200) { if (res.status === 200) {
window.location = window.location.origin+ "/store"; window.location = window.location.origin+ "/entgra";
} }
}).catch(function (error) { }).catch(function (error) {
if (error.response.status === 400) { if (error.response.status === 400) {

@ -1,157 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Layout, Menu, Icon} from 'antd';
const {Header, Content, Footer} = Layout;
import {Link} from "react-router-dom";
import RouteWithSubRoutes from "../../components/RouteWithSubRoutes";
import {Switch} from 'react-router';
import axios from "axios";
import "../../App.css";
import {withConfigContext} from "../../context/ConfigContext";
import Logout from "./logout/Logout";
const {SubMenu} = Menu;
class Dashboard extends React.Component {
constructor(props) {
super(props);
this.state = {
routes: props.routes,
selectedKeys: [],
deviceTypes: []
};
this.logo = this.props.context.theme.logo;
}
componentDidMount() {
this.getDeviceTypes();
}
getDeviceTypes = () => {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt + "/device-types"
).then(res => {
if (res.status === 200) {
const deviceTypes = JSON.parse(res.data.data);
this.setState({
deviceTypes,
loading: false,
});
}
}).catch((error) => {
if (error.hasOwnProperty("response") && error.response.status === 401) {
window.location.href = window.location.origin + '/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load device types.",
});
}
this.setState({
loading: false
});
});
};
changeSelectedMenuItem = (key) => {
this.setState({
selectedKeys: [key]
})
};
render() {
const config = this.props.context;
const {selectedKeys, deviceTypes} = this.state;
return (
<div>
<Layout className="layout">
<Header style={{paddingLeft: 0, paddingRight: 0}}>
<div className="logo-image">
<img alt="logo" src={this.logo}/>
</div>
<Menu
theme="light"
mode="horizontal"
defaultSelectedKeys={selectedKeys}
style={{lineHeight: '64px'}}
>
{
deviceTypes.map((deviceType) => {
const platform = deviceType.name;
const defaultPlatformIcons = config.defaultPlatformIcons;
let icon = defaultPlatformIcons.default.icon;
let theme = defaultPlatformIcons.default.theme;
if (defaultPlatformIcons.hasOwnProperty(platform)) {
icon = defaultPlatformIcons[platform].icon;
theme = defaultPlatformIcons[platform].theme;
}
return (
<Menu.Item key={platform}>
<Link to={"/store/" + platform}>
<Icon type={icon} theme={theme}/>
{platform}
</Link>
</Menu.Item>
);
})
}
<Menu.Item key="web-clip"><Link to="/store/web-clip"><Icon type="upload"/>Web
Clips</Link></Menu.Item>
<SubMenu className="profile"
title={
<span className="submenu-title-wrapper">
<Icon type="user"/>
Profile
</span>
}
>
<Logout/>
</SubMenu>
</Menu>
</Header>
</Layout>
<Layout>
<Content style={{padding: '0 0'}}>
<Switch>
{this.state.routes.map((route) => (
<RouteWithSubRoutes changeSelectedMenuItem={this.changeSelectedMenuItem}
key={route.path} {...route} />
))}
</Switch>
</Content>
<Footer style={{textAlign: 'center'}}>
©2019 entgra.io
</Footer>
</Layout>
</div>
);
}
}
export default withConfigContext(Dashboard);

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

@ -1,84 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import { Upload, Icon, message } from 'antd';
function getBase64(img, callback) {
const reader = new FileReader();
reader.addEventListener('load', () => callback(reader.result));
reader.readAsDataURL(img);
}
function beforeUpload(file) {
const isJPG = file.type === 'image/jpeg';
if (!isJPG) {
message.error('You can only upload JPG file!');
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('Image must smaller than 2MB!');
}
return isJPG && isLt2M;
}
class IconImage extends React.Component {
state = {
loading: false,
};
handleChange = (info) => {
if (info.file.status === 'uploading') {
this.setState({ loading: true });
return;
}
if (info.file.status === 'done') {
// Get this url from response in real world.
getBase64(info.file.originFileObj, imageUrl => this.setState({
imageUrl,
loading: false,
}));
}
}
render() {
const uploadButton = (
<div>
<Icon type={this.state.loading ? 'loading' : 'plus'} />
<div className="ant-upload-text">Upload</div>
</div>
);
const imageUrl = this.state.imageUrl;
return (
<Upload
name="avatar"
listType="picture-card"
className="avatar-uploader"
showUploadList={false}
action="//jsonplaceholder.typicode.com/posts/"
beforeUpload={beforeUpload}
onChange={this.handleChange}
>
{imageUrl ? <img src={imageUrl} alt="avatar" /> : uploadButton}
</Upload>
);
}
}
export default IconImage;

@ -1,170 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import {Form, Input, Button, Select, Divider, Tag, Tooltip, Icon, Checkbox, Row, Col} from "antd";
import styles from './Style.less';
const { Option } = Select;
const { TextArea } = Input;
const InputGroup = Input.Group;
const formItemLayout = {
labelCol: {
span: 8,
},
wrapperCol: {
span: 16,
},
};
class EditableTagGroup extends React.Component {
state = {
tags: [],
inputVisible: false,
inputValue: '',
};
handleClose = (removedTag) => {
const tags = this.state.tags.filter(tag => tag !== removedTag);
// console.log(tags);
this.setState({ tags });
}
showInput = () => {
this.setState({ inputVisible: true }, () => this.input.focus());
}
handleInputChange = (e) => {
this.setState({ inputValue: e.target.value });
}
handleInputConfirm = () => {
const { inputValue } = this.state;
let { tags } = this.state;
if (inputValue && tags.indexOf(inputValue) === -1) {
tags = [...tags, inputValue];
}
// console.log(tags);
this.setState({
tags,
inputVisible: false,
inputValue: '',
});
}
saveInputRef = input => this.input = input
render() {
const { tags, inputVisible, inputValue } = this.state;
return (
<div>
{tags.map((tag, index) => {
const isLongTag = tag.length > 20;
const tagElem = (
<Tag key={tag} closable={index !== 0} onClose={() => this.handleClose(tag)}>
{isLongTag ? `${tag.slice(0, 20)}...` : tag}
</Tag>
);
return isLongTag ? <Tooltip title={tag} key={tag}>{tagElem}</Tooltip> : tagElem;
})}
{inputVisible && (
<Input
ref={this.saveInputRef}
type="text"
size="small"
style={{ width: 78 }}
value={inputValue}
onChange={this.handleInputChange}
onBlur={this.handleInputConfirm}
onPressEnter={this.handleInputConfirm}
/>
)}
{!inputVisible && (
<Tag
onClick={this.showInput}
style={{ background: '#fff', borderStyle: 'dashed' }}
>
<Icon type="plus" /> New Tag
</Tag>
)}
</div>
);
}
}
class Step1 extends React.Component {
render() {
return (
<div>
<Form layout="horizontal" className={styles.stepForm} hideRequiredMark>
<Form.Item {...formItemLayout} label="Platform">
<Select placeholder="ex: android">
<Option value="Android">Android</Option>
<Option value="iOS">iOS</Option>
</Select>
</Form.Item>
<Form.Item {...formItemLayout} label="Type">
<Select value="Enterprise">
<Option value="Enterprise" selected>Enterprise</Option>
</Select>
</Form.Item>
<Form.Item {...formItemLayout} label="Name">
<Input placeholder="App Name" />
</Form.Item>
<Form.Item {...formItemLayout} label="Description">
<TextArea placeholder="Enter the description" rows={4} />
</Form.Item>
<Form.Item {...formItemLayout} label="Category">
<Select placeholder="Select a category">
<Option value="travel">Travel</Option>
<Option value="entertainment">Entertainment</Option>
</Select>
</Form.Item>
<Form.Item {...formItemLayout} label="Tags">
<EditableTagGroup/>
</Form.Item>
<Form.Item {...formItemLayout} label="Price">
<Input prefix="$" placeholder="00.00" />
</Form.Item>
<Form.Item {...formItemLayout} label="Share with all tenents?">
<Checkbox > </Checkbox>
</Form.Item>
<Form.Item {...formItemLayout} label="Meta Daa">
<InputGroup>
<Row gutter={8}>
<Col span={5}>
<Input placeholder="Key" />
</Col>
<Col span={10}>
<Input placeholder="value" />
</Col>
<Col span={4}>
<Button type="dashed" shape="circle" icon="plus" />
</Col>
</Row>
</InputGroup>
</Form.Item>
</Form>
</div>
);
}
}
export default Step1;

@ -1,29 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react"
class Step2 extends React.Component {
render() {
return (
<p>tttoooeeee</p>
);
}
}
export default Step2;

@ -1,29 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react"
class Step3 extends React.Component {
render() {
return (
<p>tttoooeeee</p>
);
}
}
export default Step3;

@ -1,22 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.stepForm {
max-width: 500px;
margin: 40px auto 0;
}

@ -1,67 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import { Upload, Icon, Modal} from 'antd';
class UploadScreenshots extends React.Component {
state = {
previewVisible: false,
previewImage: '',
fileList: [],
};
handleCancel = () => this.setState({ previewVisible: false });
handlePreview = (file) => {
this.setState({
previewImage: file.url || file.thumbUrl,
previewVisible: true,
});
};
handleChange = ({ fileList }) => this.setState({ fileList });
render() {
const { previewVisible, previewImage, fileList } = this.state;
const uploadButton = (
<div>
<Icon type="plus" />
<div className="ant-upload-text">Upload</div>
</div>
);
return (
<div className="clearfix">
<Upload
action="//jsonplaceholder.typicode.com/posts/"
listType="picture-card"
fileList={fileList}
onPreview={this.handlePreview}
onChange={this.handleChange}
>
{fileList.length >= 3 ? null : uploadButton}
</Upload>
<Modal visible={previewVisible} footer={null} onCancel={this.handleCancel}>
<img alt="example" style={{ width: '100%' }} src={previewImage} />
</Modal>
</div>
);
}
}
export default UploadScreenshots;

@ -1,67 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import { Upload, Icon, Modal} from 'antd';
class AddTagModal extends React.Component {
state = {
previewVisible: false,
previewImage: '',
fileList: [],
};
handleCancel = () => this.setState({ previewVisible: false });
handlePreview = (file) => {
this.setState({
previewImage: file.url || file.thumbUrl,
previewVisible: true,
});
};
handleChange = ({ fileList }) => this.setState({ fileList });
render() {
const { previewVisible, previewImage, fileList } = this.state;
const uploadButton = (
<div>
<Icon type="plus" />
<div className="ant-upload-text">Upload</div>
</div>
);
return (
<div className="clearfix">
<Upload
action="//jsonplaceholder.typicode.com/posts/"
listType="picture-card"
fileList={fileList}
onPreview={this.handlePreview}
onChange={this.handleChange}
>
{fileList.length >= 3 ? null : uploadButton}
</Upload>
<Modal visible={previewVisible} footer={null} onCancel={this.handleCancel}>
<img alt="example" style={{ width: '100%' }} src={previewImage} />
</Modal>
</div>
);
}
}
export default AddTagModal;

@ -1,45 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import AppList from "../../../components/apps/AppList";
class Apps extends React.Component {
routes;
constructor(props) {
super(props);
this.routes = props.routes;
}
render() {
const {deviceType} = this.props.match.params;
return (
<div>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 760}}>
{deviceType!==null && <AppList changeSelectedMenuItem={this.props.changeSelectedMenuItem} deviceType={deviceType}/>}
</div>
</div>
);
}
}
export default Apps;

@ -1,131 +0,0 @@
/*
* Copyright (c) 2019, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from "react";
import '../../../../App.css';
import {Skeleton, Typography, Row, Col, Card, message, notification, Breadcrumb, Icon} from "antd";
import ReleaseView from "../../../../components/apps/release/ReleaseView";
import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext";
import {Link} from "react-router-dom";
const {Title} = Typography;
class Release extends React.Component {
routes;
constructor(props) {
super(props);
this.routes = props.routes;
this.state = {
loading: true,
app: null,
uuid: null
};
}
componentDidMount() {
const {uuid, deviceType} = this.props.match.params;
this.fetchData(uuid);
this.props.changeSelectedMenuItem(deviceType);
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (prevState.uuid !== this.state.uuid) {
const {uuid, deviceType} = this.props.match.params;
this.fetchData(uuid);
this.props.changeSelectedMenuItem(deviceType);
}
}
fetchData = (uuid) => {
const config = this.props.context;
//send request to the invoker
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.store + "/applications/" + uuid,
).then(res => {
if (res.status === 200) {
let app = res.data.data;
this.setState({
app: app,
loading: false,
uuid: uuid
})
}
}).catch((error) => {
console.log(error);
if (error.hasOwnProperty("response") && error.response.status === 401) {
//todo display a popop with error
message.error('You are not logged in');
window.location.href = window.location.origin + '/store/login';
} else {
notification["error"]({
message: "There was a problem",
duration: 0,
description:
"Error occurred while trying to load releases.",
});
}
this.setState({loading: false});
});
};
render() {
const {app, loading} = this.state;
const {deviceType} = this.props.match.params;
let content = <Title level={3}>No Releases Found</Title>;
let appName = "loading...";
if (app != null && app.applicationReleases.length !== 0) {
content = <ReleaseView app={app} deviceType={deviceType}/>;
appName = app.name;
}
return (
<div style={{background: '#f0f2f5', minHeight: 780}}>
<Row style={{padding: 10}}>
<Col lg={4}>
</Col>
<Col lg={16} md={24} style={{padding: 3}}>
<Breadcrumb style={{paddingBottom: 16}}>
<Breadcrumb.Item>
<Link to={"/store/"+deviceType}><Icon type="home"/> {deviceType + " apps"} </Link>
</Breadcrumb.Item>
<Breadcrumb.Item>{appName}</Breadcrumb.Item>
</Breadcrumb>
<Card>
<Skeleton loading={loading} avatar={{size: 'large'}} active paragraph={{rows: 8}}>
{content}
</Skeleton>
</Card>
</Col>
</Row>
</div>
);
}
}
export default withConfigContext(Release);

@ -23,7 +23,7 @@ const configurations = require("./public/conf/config.json");
const config = { const config = {
devtool: "source-map", devtool: "source-map",
output: { output: {
publicPath: '/store/' publicPath: '/entgra/'
}, },
watch: false, watch: false,
resolve: { resolve: {

Loading…
Cancel
Save