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

Create APPM's basic store view

See merge request entgra/carbon-device-mgt!110
feature/appm-store/pbac
Dharmakeerthi Lasantha 6 years ago
commit 443a083048

@ -8,7 +8,7 @@
.release .release-icon img{
width: 100%;
border-radius: 100%;
border-radius: 28%;
}
.release .release-title{

@ -1,44 +1,63 @@
import React from "react";
import {Avatar, Row, Col, Typography, Button} from "antd";
import {Divider, Row, Col, Typography, Button} from "antd";
import "../../../App.css";
import config from "../../../../public/conf/config.json";
const {Title, Text} = Typography;
const {Title, Text, Paragraph} = Typography;
class ReleaseView extends React.Component {
render() {
const release = this.props.release;
console.log(release);
return (
<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>{release.version}</Text><br/>
<Text type="secondary">{release.description}</Text><br/>
</Col>
<Col xl={8} md={10} sm={24} xs={24} style={{float: "right"}}>
<div>
<Button.Group style={{float: "right"}}>
<Button htmlType="button" icon="shop">Open in store</Button>
<Button htmlType="button" type="primary" icon="edit">edit</Button>
</Button.Group>
</div>
</Col>
</Row>
<br/>
<Row>
{release.screenshots.map((screenshotUrl)=>{
return (
<Col key={"col-"+screenshotUrl} lg={6} md={8} xs={8} className="release-screenshot">
<img key={screenshotUrl} src={screenshotUrl}/>
</Col>
)
})}
</Row>
<div>
<div className="release">
<Row>
<Col xl={4} sm={6} xs={8} className="release-icon">
<img src={release.iconPath} alt="icon"/>
</Col>
<Col xl={10} sm={11} className="release-title">
<Title level={2}>App Name</Title>
<Text>{release.version}</Text><br/>
<Text type="secondary">{release.description}</Text><br/>
</Col>
<Col xl={8} md={10} sm={24} xs={24} style={{float: "right"}}>
<div>
<Button.Group style={{float: "right"}}>
<Button htmlType="button" icon="edit">edit</Button>
<Button htmlType="button"
type="primary"
icon="shop"
disabled={release.currentStatus !== "PUBLISHED"}
onClick={() => {
window.open("https://"+ config.serverConfig.hostname + ':' + config.serverConfig.httpsPort+"/store/apps/"+release.uuid)
}}>
Open in store
</Button>
</Button.Group>
</div>
</Col>
</Row>
<Divider/>
<Row>
{release.screenshots.map((screenshotUrl) => {
return (
<Col key={"col-" + screenshotUrl} lg={6} md={8} xs={8} className="release-screenshot">
<img key={screenshotUrl} src={screenshotUrl}/>
</Col>
)
})}
</Row>
<Divider/>
<Paragraph type="secondary" ellipsis={{rows: 3, expandable: true}}>
Ant Design, a design language for background applications, is refined by Ant UED Team. Ant
Design, a design language for background applications, is refined by Ant UED Team. Ant Design,
a design language for background applications, is refined by Ant UED Team. Ant Design, a
design language for background applications, is refined by Ant UED Team. Ant Design, a design
language for background applications, is refined by Ant UED Team. Ant Design, a design
language for background applications, is refined by Ant UED Team.
</Paragraph>
</div>
</div>
);
}

@ -1,7 +1,7 @@
{
"name": "store",
"version": "1.0.0",
"description": "WSO2 IoT Server App Publisher",
"description": "WSO2 IoT Server App Store",
"main": "App.js",
"proxy": "http://localhost:3001",
"repository": {
@ -12,11 +12,23 @@
"dependencies": {
"acorn": "^6.1.1",
"antd": "^3.15.0",
"axios": "^0.18.0",
"d3": "^5.9.2",
"dagre": "^0.8.4",
"keymirror": "^0.1.1",
"rc-viewer": "0.0.9",
"react": "^16.8.4",
"react-d3-graph": "^2.0.2",
"react-dom": "^16.8.4",
"react-highlight-words": "^0.16.0",
"react-image-viewer-zoom": "^1.0.36",
"react-router": "latest",
"react-router-config": "^5.0.0",
"react-router-dom": "latest",
"react-scripts": "2.1.8"
"react-scripts": "2.1.8",
"react-star-ratings": "^2.3.0",
"redux-thunk": "^2.3.0",
"storm-react-diagrams": "^5.2.1"
},
"devDependencies": {
"@babel/core": "^7.0.0",
@ -34,6 +46,7 @@
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"img-loader": "^3.0.1",
"json-loader": "^0.5.7",
"less": "^3.9.0",
"less-loader": "^4.1.0",
"mini-css-extract-plugin": "^0.5.0",
@ -48,6 +61,7 @@
"react": "^15.6.2",
"react-dom": "^15.6.2",
"react-intl": "^2.4.0",
"react-redux": "^7.0.2",
"redux": "^4.0.1",
"sass-loader": "^6.0.7",
"style-loader": "^0.18.2",

@ -3,10 +3,16 @@
"type": "default",
"value": "lightBaseTheme"
},
"config": {
"serverConfig": {
"hostname": "localhost",
"httpsPort": "9443",
"apiPort": "8243"
"invokerUri": "/api/application-mgt-handler/v1.0/invoke",
"loginUri": "/api/application-mgt-handler/v1.0/login"
},
"serverUrl" : "https://localhost:9443"
"serverUrl" : "https://localhost:9443",
"defaultPlatformIcons": {
"default": "http://www.newdesignfile.com/postpic/2015/08/square-app-icon-blue_77131.png",
"android": "data:image/png;base64,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",
"ios" : "data:image/png;base64,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"
}
}

@ -1,6 +1,6 @@
{
"short_name": "App Publisher",
"name": "WSO2 IoT App Publisher",
"short_name": "App Store",
"name": "WSO2 IoT App Store",
"icons": [
{
"src": "images/favicon.png",

@ -328,8 +328,8 @@ body {
margin-top: 128px !important;
}
/* Holds the app publisher pages. */
.publisher-card {
/* Holds the app pages. */
.store-card {
height: auto;
background-color: white;
box-shadow: 2px 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);

@ -1,17 +0,0 @@
const express = require('express');
const bodyParser = require('body-parser');
const pino = require('express-pino-logger')();
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(pino);
app.get('/api/greeting', (req, res) => {
const name = req.query.name || 'World';
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({ greeting: `Hello ${name}!` }));
});
app.listen(3001, () =>
console.log('Express server is running on localhost:3001')
);

@ -1,33 +1,45 @@
.App {
text-align: center;
.ant-upload.ant-upload-drag {
height: 170px;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 40vmin;
pointer-events: none;
.release-icon img{
width: 100%;
border-radius: 28%;
}
.release .release-icon{
margin-right: 15px;
}
.appCard .release-icon{
margin-bottom: 10px;
}
.release .release-title{
margin-left: 15px;
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
.release .release-screenshot img{
width: 100%;
border-radius: 15px;
padding: 5px;
}
.App-link {
color: #61dafb;
.logo {
width: 120px;
height: 31px;
margin: 0 0 16px 20px;
float: left;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
.logo img{
height: 35px;
}
.main-container{
background: #f0f2f5;
min-height: 780px
}
@media only screen and (min-width: 768px) {
.main-container{
padding: 24px;
}
}

@ -1,22 +1,35 @@
import React from "react";
import "antd/dist/antd.css";
import { renderRoutes } from "react-router-config";
import RouteWithSubRoutes from "./components/RouteWithSubRoutes";
import {
BrowserRouter as Router,
Link, Redirect, Switch,
} from 'react-router-dom';
class App extends React.Component {
routes;
constructor(props) {
super(props);
this.state = {
route : props.route
}
this.routes = props.routes;
}
render() {
return (
<div>
{renderRoutes(this.state.route.routes)}
</div>
);
}
render() {
console.log(this.routes);
return (
<Router>
<div>
<Switch>
<Redirect exact from="/store" to="/store/apps"/>
{this.routes.map((route) => (
<RouteWithSubRoutes key={route.path} {...route} />
))}
</Switch>
</div>
</Router>
);
}
}
export default App;

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

@ -0,0 +1,71 @@
import {
Skeleton, Switch, Card, Icon, Avatar, Typography, Col, Row, Rate
} from 'antd';
import React from "react";
import {openReleasesModal} from "../../js/actions";
import {connect} from "react-redux";
import {Link} from "react-router-dom";
import "../../App.css";
import StarRatings from 'react-star-ratings';
const {Meta} = Card;
const {Text, Title} = Typography;
const mapDispatchToProps = dispatch => ({
openReleasesModal: (app) => dispatch(openReleasesModal(app))
});
class ConnectedAppCard 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];
console.log(this.props);
const description = (
<div className="appCard">
<Link to={"/store/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>
);
}
}
const AppCard = connect(null, mapDispatchToProps)(ConnectedAppCard);
export default AppCard;

@ -0,0 +1,38 @@
import React from "react";
import AppCard from "./AppCard";
import {Col, Row} from "antd";
import {connect} from "react-redux";
import {getApps} from "../../js/actions";
// connecting state.apps with the component
const mapStateToProps= state => {
return {apps : state.apps}
};
class ConnectedAppList extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.getApps();
}
render() {
return (
<Row gutter={16}>
{this.props.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>
);
}
}
const AppList = connect(mapStateToProps,{getApps})(ConnectedAppList);
export default AppList;

@ -0,0 +1,85 @@
import React from "react";
import {Modal, Typography,List, Avatar} from 'antd';
import {connect} from 'react-redux';
import {Link} from "react-router-dom";
// connecting state.releaseView with the component
const mapStateToProps = state => {
return {releaseView: state.releaseView}
};
const Text = Typography;
class ConnectedReleaseModal extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
app: null
};
}
componentWillReceiveProps(nextProps) {
if (nextProps !== this.props) {
this.setState({
visible: nextProps.releaseView.visible,
app: nextProps.releaseView.app
})
}
}
showModal = () => {
this.setState({
visible: true,
});
};
handleOk = (e) => {
this.setState({
visible: false,
});
};
handleCancel = (e) => {
this.setState({
visible: false,
});
};
render() {
if (this.props.releaseView.app != null) {
const app = this.props.releaseView.app;
return (
<div>
<Modal
title={app.name}
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
>
<p>Some contents...</p>
<List
itemLayout="horizontal"
dataSource={app.applicationReleases}
renderItem={release => (
<List.Item>
<List.Item.Meta
avatar={<Avatar src={release.iconPath} />}
title={<Link to={"/store/apps/"+release.uuid}>{release.version}</Link>}
description={release.description}
/>
</List.Item>
)}
/>
</Modal>
</div>
);
} else {
return null;
}
}
}
const ReleaseModal = connect(mapStateToProps, null)(ConnectedReleaseModal);
export default ReleaseModal;

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

@ -0,0 +1,91 @@
import React from "react";
import {Row, Typography, Icon} from "antd";
import StarRatings from "react-star-ratings";
import "./DetailedRating.css";
import {connect} from "react-redux";
import {getDetailedRating} from "../../../js/actions";
const { Text } = Typography;
// connecting state. with the component
const mapStateToProps= state => {
return {detailedRating : state.detailedRating}
};
const mapDispatchToProps = dispatch => ({
getDetailedRating: (uuid) => dispatch(getDetailedRating(uuid))
});
class ConnectedDetailedRating extends React.Component{
componentDidMount() {
this.props.getDetailedRating(this.props.uuid);
}
render() {
const detailedRating = this.props.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);
console.log(maximumRating);
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: (ratingVariety["5"]/maximumRating*100)+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">4</span>
<span className="bar rate-4" style={{width: (ratingVariety["4"]/maximumRating*100)+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">3</span>
<span className="bar rate-3" style={{width: (ratingVariety["3"]/maximumRating*100)+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">2</span>
<span className="bar rate-2" style={{width: (ratingVariety["2"]/maximumRating*100)+"%"}}> </span>
</div>
<div className="bar-container">
<span className="number">1</span>
<span className="bar rate-1" style={{width: (ratingVariety["1"]/maximumRating*100)+"%"}}> </span>
</div>
</div>
</Row>
);
}
}
const DetailedRating = connect(mapStateToProps,mapDispatchToProps)(ConnectedDetailedRating);
export default DetailedRating;

@ -0,0 +1,61 @@
import React from "react";
import {Divider, Row, Col, Typography, Button, Rate} from "antd";
import "../../../App.css";
import ImgViewer from "../../apps/release/images/ImgViewer";
import StarRatings from "react-star-ratings";
import DetailedRating from "./DetailedRating";
const {Title, Text, Paragraph} = Typography;
class ReleaseView extends React.Component {
render() {
const release = this.props.release;
return (
<div>
<div className="release">
<Row>
<Col xl={4} sm={6} xs={8} className="release-icon">
<img src={release.iconPath} alt="icon"/>
</Col>
<Col xl={10} sm={11} className="release-title">
<Title level={2}>App Name</Title>
<Text>Version : {release.version}</Text><br/><br/>
<StarRatings
rating={release.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 htmlType="button" icon="usergroup-add">Enterprise Install</Button>
<Button 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/>
<Text>REVIEWS</Text>
<Row>
<Col lg={18}>
<DetailedRating uuid={release.uuid}/>
</Col>
</Row>
</div>
</div>
);
}
}
export default ReleaseView;

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

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

@ -1,41 +1,53 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { renderRoutes } from "react-router-config";
import Dashboard from "./pages/dashboard/Dashboard"
import App from "./App";
import Login from "./pages/Login";
import {BrowserRouter} from "react-router-dom";
import Dashboard from "./pages/dashboard/Dashboard";
import Apps from "./pages/dashboard/apps/Apps";
import Release from "./pages/dashboard/apps/release/Release";
import AddNewApp from "./pages/dashboard/add-new-app/AddNewApp";
import './index.css';
import store from "./js/store/index";
import {Provider} from "react-redux";
const routes = [
{
component: App,
path: '/store/login',
exact: true,
component: Login
},
{
path: '/store/apps',
exact: false,
component: Dashboard,
routes: [
{
path: "/publisher",
exact: true,
component: Dashboard,
routes: [
{
path: "/publisher/a",
component: Login
}
]
path: '/store/apps',
component: Apps,
exact: true
},
{
path: "/publisher/login",
component: Login
path: '/store/apps/new-app',
component: AddNewApp,
exact: true
},
{
path: '/store/apps/:uuid',
exact: true,
component: Release
}
]
}
];
ReactDOM.render( <BrowserRouter>
{/* kick it all off with the root route */}
{renderRoutes(routes)}
</BrowserRouter>, document.getElementById('root'));
ReactDOM.render(
<Provider store={store}>
<App routes={routes}/>
</Provider>,
document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.

@ -0,0 +1,168 @@
import axios from "axios";
import ActionTypes from "../constants/ActionTypes";
import config from "../../../public/conf/config.json";
export const getApps = () => dispatch => {
const request = "method=post&content-type=application/json&payload={}&api-endpoint=/application-mgt-store/v1.0/applications";
return axios.post('https://' + config.serverConfig.hostname + ':' + config.serverConfig.httpsPort + config.serverConfig.invokerUri, request
).then(res => {
if (res.status === 200) {
let apps = [];
if (res.data.data.hasOwnProperty("applications")) {
apps = res.data.data.applications;
}
dispatch({type: ActionTypes.GET_APPS, payload: apps});
}
}).catch(function (error) {
if (error.response.status === 401) {
window.location.href = 'https://localhost:9443/store/login';
}
});
};
export const getRelease = (uuid) => dispatch => {
const request = "method=get&content-type=application/json&payload={}&api-endpoint=/application-mgt-store/v1.0/applications/" + uuid;
return axios.post('https://' + config.serverConfig.hostname + ':' + config.serverConfig.httpsPort + config.serverConfig.invokerUri, request
).then(res => {
if (res.status === 200) {
let release = res.data.data.applicationReleases[0];
dispatch({
type: ActionTypes.GET_RELEASE,
payload: {
release: release,
releaseLoading: false
}
});
}
}).catch(function (error) {
if (error.response.status === 401) {
window.location.href = 'https://localhost:9443/store/login';
}else if(error.response.status===404){
dispatch({
type: ActionTypes.GET_RELEASE,
payload: {
release: null,
releaseLoading: false
}});
}
});
};
export const openReleasesModal = (app) => dispatch => {
dispatch({
type: ActionTypes.OPEN_RELEASES_MODAL,
payload: {
app: app
}
});
};
export const openLifecycleModal = (nextState) => dispatch => {
dispatch({
type: ActionTypes.OPEN_LIFECYCLE_MODAL,
payload: {
nextState: nextState
}
});
};
export const closeLifecycleModal = () => dispatch => {
dispatch({
type: ActionTypes.CLOSE_LIFECYCLE_MODAL
});
};
export const setLoading = (stateToLoad) => dispatch => {
dispatch({
type: ActionTypes.SET_LOADING_STATE,
payload: {
stateToLoad: stateToLoad
}
});
};
export const getLifecycle = () => dispatch => {
const request = "method=get&content-type=application/json&payload={}&api-endpoint=/application-mgt-store/v1.0/applications/lifecycle-config";
return axios.post('https://' + config.serverConfig.hostname + ':' + config.serverConfig.httpsPort + config.serverConfig.invokerUri, request
).then(res => {
if (res.status === 200) {
let lifecycle = res.data.data;
dispatch({type: ActionTypes.GET_LIFECYCLE, payload: lifecycle});
}
}).catch(function (error) {
if (error.response.status === 401) {
window.location.href = 'https://localhost:9443/store/login';
}
});
};
export const updateLifecycleState = (uuid, nextState, reason) => dispatch => {
const payload = {
action: nextState,
reason: reason
};
const request = "method=post&content-type=application/json&payload=" + JSON.stringify(payload) + "&api-endpoint=/application-mgt-store/v1.0/applications/life-cycle/" + uuid;
return axios.post('https://' + config.serverConfig.hostname + ':' + config.serverConfig.httpsPort + config.serverConfig.invokerUri, request
).then(res => {
if (res.status === 201) {
let release = res.data.data;
dispatch({type: ActionTypes.UPDATE_LIFECYCLE_STATE, payload: release});
}else {
alert("error");
dispatch({
type: ActionTypes.CLOSE_LIFECYCLE_MODAL
});
}
}).catch(function (error) {
if (error.response.status === 401) {
window.location.href = 'https://localhost:9443/store/login';
} else if (error.response.status === 500) {
alert("error");
dispatch({
type: ActionTypes.CLOSE_LIFECYCLE_MODAL
});
}
});
};
export const getDetailedRating = (uuid) => dispatch => {
const request = "method=get&content-type=application/json&payload={}&api-endpoint=/application-mgt-store/v1.0/reviews/"+uuid+"/rating";
return axios.post('https://' + config.serverConfig.hostname + ':' + config.serverConfig.httpsPort + config.serverConfig.invokerUri, request
).then(res => {
if (res.status === 200) {
let detailedRating = res.data.data;
dispatch({type: ActionTypes.GET_DETAILED_RATING, payload: detailedRating});
}
}).catch(function (error) {
if (error.response.status === 401) {
window.location.href = 'https://localhost:9443/store/login';
} else{
dispatch({
type: ActionTypes.GET_DETAILED_RATING, payload: null
});
}
});
};

@ -0,0 +1,17 @@
import keyMirror from 'keymirror';
const ActionTypes = keyMirror({
LOGIN: null,
GET_APPS: null,
OPEN_RELEASES_MODAL: null,
CLOSE_RELEASES_MODAL: null,
GET_RELEASE: null,
GET_LIFECYCLE: null,
OPEN_LIFECYCLE_MODAL: null,
CLOSE_LIFECYCLE_MODAL: null,
UPDATE_LIFECYCLE_STATE: null,
SET_LOADING_STATE: null,
GET_DETAILED_RATING: null
});
export default ActionTypes;

@ -0,0 +1,83 @@
import ActionTypes from "../constants/ActionTypes";
const initialState = {
apps: [],
releaseView: {
visible: false,
app: null
},
release: null,
lifecycle: null,
lifecycleModal: {
visible: false,
nextState: null
},
loadingState: {
release: true
},
detailedRating: null
};
function rootReducer(state = initialState, action) {
if (action.type === ActionTypes.GET_APPS) {
return Object.assign({}, state, {
apps: action.payload
});
} else if (action.type === ActionTypes.OPEN_RELEASES_MODAL) {
return Object.assign({}, state, {
releaseView: {
visible: true,
app: action.payload.app
}
});
} else if (action.type === ActionTypes.GET_RELEASE) {
let loadingState = {...state.loadingState};
loadingState.release = action.payload.releaseLoading;
return Object.assign({}, state, {
release: action.payload.release,
loadingState: loadingState
});
} else if (action.type === ActionTypes.GET_LIFECYCLE) {
return Object.assign({}, state, {
lifecycle: action.payload
});
} else if (action.type === ActionTypes.OPEN_LIFECYCLE_MODAL) {
return Object.assign({}, state, {
lifecycleModal: {
visible: true,
nextState: action.payload.nextState
}
});
} else if (action.type === ActionTypes.CLOSE_LIFECYCLE_MODAL) {
return Object.assign({}, state, {
lifecycleModal: {
visible: false,
nextState: null
}
});
} else if (action.type === ActionTypes.UPDATE_LIFECYCLE_STATE) {
return Object.assign({}, state, {
lifecycleModal: {
visible: false,
nextState: null,
},
release: action.payload
});
} else if (action.type === ActionTypes.SET_LOADING_STATE) {
let loadingState = {...state.loadingState};
loadingState[action.payload.stateToLoad] = true;
return Object.assign({}, state, {
loadingState: loadingState
});
} else if (action.type === ActionTypes.GET_DETAILED_RATING) {
return Object.assign({}, state, {
detailedRating: action.payload
});
}
return state;
}
export default rootReducer;

@ -0,0 +1,5 @@
import { createStore, applyMiddleware } from "redux";
import rootReducer from "../reducers/index";
import thunk from "redux-thunk";
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;

@ -0,0 +1,56 @@
import * as dagre from "dagre";
import * as _ from "lodash";
const size = {
width: 60,
height: 60
};
export function distributeElements(model) {
let clonedModel = _.cloneDeep(model);
let nodes = distributeGraph(clonedModel);
nodes.forEach(node => {
let modelNode = clonedModel.nodes.find(item => item.id === node.id);
modelNode.x = node.x - node.width / 2;
modelNode.y = node.y - node.height / 2;
});
return clonedModel;
}
function distributeGraph(model) {
let nodes = mapElements(model);
let edges = mapEdges(model);
let graph = new dagre.graphlib.Graph();
graph.setGraph({});
graph.setDefaultEdgeLabel(() => ({}));
//add elements to dagre graph
nodes.forEach(node => {
graph.setNode(node.id, node.metadata);
});
edges.forEach(edge => {
if (edge.from && edge.to) {
graph.setEdge(edge.from, edge.to);
}
});
//auto-distribute
dagre.layout(graph);
return graph.nodes().map(node => graph.node(node));
}
function mapElements(model) {
// dagre compatible format
return model.nodes.map(node => ({ id: node.id, metadata: { ...size, id: node.id } }));
}
function mapEdges(model) {
// returns links which connects nodes
// we check are there both from and to nodes in the model. Sometimes links can be detached
return model.links
.map(link => ({
from: link.source,
to: link.target
}))
.filter(
item => model.nodes.find(node => node.id === item.from) && model.nodes.find(node => node.id === item.to)
);
}

@ -1,7 +1,11 @@
import React from "react";
import {Typography, Row, Col, Form, Icon, Input, Button, Checkbox,} from 'antd';
import {Typography, Row, Col, Form, Icon, Input, Button, Checkbox} from 'antd';
import styles from './Login.less';
import axios from 'axios';
import config from "../../public/conf/config.json";
const {Title} = Typography;
const {Text} = Typography;
class Login extends React.Component {
render() {
@ -33,33 +37,77 @@ class Login extends React.Component {
}
class NormalLoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
inValid: false,
loading : false
};
}
handleSubmit = (e) => {
const thisForm = this;
e.preventDefault();
this.props.form.validateFields((err, values) => {
thisForm.setState({
inValid: false
});
if (!err) {
thisForm.setState({
loading: true
});
console.log('Received values of form: ', values);
let data = "username=" + values.username + "&password=" + values.password + "&platform=store";
axios.post('https://'+config.serverConfig.hostname+':'+config.serverConfig.httpsPort+config.serverConfig.loginUri, data
).then(res => {
if (res.status === 200) {
window.location = res.data.url;
}
}).catch(function (error) {
if (error.response.status === 400) {
thisForm.setState({
inValid: true,
loading: false
});
}
});
}
});
}
};
render() {
const { getFieldDecorator } = this.props.form;
const {getFieldDecorator} = this.props.form;
let errorMsg = "";
if (this.state.inValid) {
errorMsg = <Text type="danger">Invalid Login Details</Text>;
}
let loading = "";
if (this.state.loading) {
loading = <Text type="secondary">Loading..</Text>;
}
return (
<Form onSubmit={this.handleSubmit} className="login-form">
<Form.Item>
{getFieldDecorator('userName', {
rules: [{ required: true, message: 'Please input your username!' }],
{getFieldDecorator('username', {
rules: [{required: true, message: 'Please input your username!'}],
})(
<Input style={{height: 32}} prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="Username" />
<Input style={{height: 32}} prefix={<Icon type="user" style={{color: 'rgba(0,0,0,.25)'}}/>}
placeholder="Username"/>
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('password', {
rules: [{ required: true, message: 'Please input your Password!' }],
rules: [{required: true, message: 'Please input your Password!'}],
})(
<Input style={{height: 32}} className={styles.input} prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} type="password" placeholder="Password" />
<Input style={{height: 32}} className={styles.input}
prefix={<Icon type="lock" style={{color: 'rgba(0,0,0,.25)'}}/>} type="password"
placeholder="Password"/>
)}
</Form.Item>
{loading}
{errorMsg}
<Form.Item>
{getFieldDecorator('remember', {
valuePropName: 'checked',
@ -67,17 +115,17 @@ class NormalLoginForm extends React.Component {
})(
<Checkbox>Remember me</Checkbox>
)}
<br/>
<a className="login-form-forgot" href="">Forgot password</a>
<Button block type="primary" htmlType="submit" className="login-form-button">
Log in
</Button>
Or <a href="">register now!</a>
</Form.Item>
</Form>
);
}
}
const WrappedNormalLoginForm = Form.create({ name: 'normal_login' })(NormalLoginForm);
const WrappedNormalLoginForm = Form.create({name: 'normal_login'})(NormalLoginForm);
export default Login;

@ -1,58 +1,60 @@
import React from "react";
import { Layout, Menu, Breadcrumb } from 'antd';
import {Layout, Menu, Icon} from 'antd';
const { Header, Content, Footer } = Layout;
const {Header, Content, Footer} = Layout;
import styles from './Dashboard.less';
import Logo from "../../../public/images/logo.svg";
import Login from "../Login";
import {renderRoutes} from "react-router-config";
import {NavLink} from "react-router-dom";
import {Link, NavLink} from "react-router-dom";
import RouteWithSubRoutes from "../../components/RouteWithSubRoutes"
import {Switch, Redirect} from 'react-router'
import "../../App.css";
class Dashboard extends React.Component {
constructor(props) {
super(props);
this.state = {
route : props.route
routes: props.routes
}
console.log(props);
}
render() {
return (
<Layout className="layout">
<Header>
<div style={{backgroundImage: "url(" + { Logo} + ")"}} className={styles.logo}/>
<Menu
theme="light"
mode="horizontal"
defaultSelectedKeys={['2']}
style={{ lineHeight: '64px' }}
>
<Menu.Item key="1">nav 1</Menu.Item>
<Menu.Item key="2">nav 2</Menu.Item>
<Menu.Item key="3">nav 3</Menu.Item>
</Menu>
</Header>
<Content style={{ padding: '0 50px' }}>
<Breadcrumb style={{ margin: '16px 0' }}>
<Breadcrumb.Item>Home</Breadcrumb.Item>
<Breadcrumb.Item>List</Breadcrumb.Item>
<Breadcrumb.Item>App</Breadcrumb.Item>
</Breadcrumb>
<NavLink exact to="/publisher/a" className="nav-link" >
Items
</NavLink>
<div>
<Layout className="layout">
<Header>
<div className="logo">
<img src={Logo}/>
</div>
<Menu
theme="light"
mode="horizontal"
defaultSelectedKeys={['2']}
style={{lineHeight: '64px'}}
>
<Menu.Item key="1"><Link to="/store/apps"><Icon type="appstore"/>Apps</Link></Menu.Item>
<Menu.Item key="2"><Link to="/store/apps"><Icon
type="line-chart"/>Apps</Link></Menu.Item>
<Menu.Item key="3"><Link to="/store/apps/new-app"><Icon type="upload"/>Add New
App</Link></Menu.Item>
</Menu>
</Header>
</Layout>
<Layout>
<Content style={{padding: '0 0'}}>
<Switch>
<Redirect exact from="/store" to="/store/apps"/>
{this.state.routes.map((route) => (
<RouteWithSubRoutes key={route.path} {...route} />
))}
</Switch>
{/* child routes won't render without this */}
{renderRoutes(this.state.route.routes, { someProp: "these extra props are optional" })}
<div style={{ background: '#fff', padding: 24, minHeight: 280 }}>Content</div>
</Content>
<Footer style={{ textAlign: 'center' }}>
©2019 entgra.io
</Footer>
</Layout>
</Content>
<Footer style={{textAlign: 'center'}}>
©2019 entgra.io
</Footer>
</Layout>
</div>
);
}
}

@ -1,7 +1,15 @@
.logo {
width: 120px;
height: 31px;
background: rgba(0,0,0,.2);
margin: 16px 24px 16px 0;
margin: 16px 0 16px 20px;
float: left;
img{
height: 35px;
}
}
input{
min-height: 0;
}

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

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

@ -0,0 +1,153 @@
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() {
console.log("hhhoohh");
return (
<div>
<Form layout="horizontal" className={styles.stepForm} hideRequiredMark>
<Form.Item {...formItemLayout} label="Platform">
<Select placeholder="ex: android">
<Option value="Android">Android</Option>
<Option value="iOS">iOS</Option>
</Select>
</Form.Item>
<Form.Item {...formItemLayout} label="Type">
<Select value="Enterprise">
<Option value="Enterprise" selected>Enterprise</Option>
</Select>
</Form.Item>
<Form.Item {...formItemLayout} label="Name">
<Input placeholder="App Name" />
</Form.Item>
<Form.Item {...formItemLayout} label="Description">
<TextArea placeholder="Enter the description" rows={4} />
</Form.Item>
<Form.Item {...formItemLayout} label="Category">
<Select placeholder="Select a category">
<Option value="travel">Travel</Option>
<Option value="entertainment">Entertainment</Option>
</Select>
</Form.Item>
<Form.Item {...formItemLayout} label="Tags">
<EditableTagGroup/>
</Form.Item>
<Form.Item {...formItemLayout} label="Price">
<Input prefix="$" placeholder="00.00" />
</Form.Item>
<Form.Item {...formItemLayout} label="Share with all tenents?">
<Checkbox > </Checkbox>
</Form.Item>
<Form.Item {...formItemLayout} label="Meta Daa">
<InputGroup>
<Row gutter={8}>
<Col span={5}>
<Input placeholder="Key" />
</Col>
<Col span={10}>
<Input placeholder="value" />
</Col>
<Col span={4}>
<Button type="dashed" shape="circle" icon="plus" />
</Col>
</Row>
</InputGroup>
</Form.Item>
</Form>
</div>
);
}
}
export default Step1;

@ -0,0 +1,12 @@
import React from "react"
class Step2 extends React.Component {
render() {
console.log("hhhoohh");
return (
<p>tttoooeeee</p>
);
}
}
export default Step2;

@ -0,0 +1,12 @@
import React from "react"
class Step3 extends React.Component {
render() {
console.log("hhhoohh");
return (
<p>tttoooeeee</p>
);
}
}
export default Step3;

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

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

@ -0,0 +1,60 @@
import React from "react";
import "antd/dist/antd.css";
import {PageHeader, Typography,Input, Button, Row, Col} from "antd";
import AppList from "../../../components/apps/AppList";
import ReleaseModal from "../../../components/apps/ReleaseModal";
const Search = Input.Search;
const routes = [
{
path: 'index',
breadcrumbName: 'store',
},
{
path: 'first',
breadcrumbName: 'Dashboard',
},
{
path: 'second',
breadcrumbName: 'Apps',
},
];
class Apps extends React.Component {
routes;
constructor(props) {
super(props);
this.routes = props.routes;
}
render() {
return (
<div>
<PageHeader
breadcrumb={{routes}}
/>
<div style={{background: '#f0f2f5', padding: 24, minHeight: 780}}>
<Row style={{padding:10}}>
<Col span={6} offset={18}>
<Search
placeholder="search"
onSearch={value => console.log(value)}
style={{ width: 200}}
/>
<Button style={{margin:5}}>Advanced Search</Button>
</Col>
</Row>
<ReleaseModal/>
<AppList/>
</div>
</div>
);
}
}
export default Apps;

@ -0,0 +1,104 @@
import React from "react";
import '../../../../App.css';
import {Skeleton, Typography, Row, Col, Card} from "antd";
import {connect} from "react-redux";
import ReleaseView from "../../../../components/apps/release/ReleaseView";
import {getRelease, setLoading} from "../../../../js/actions";
const {Title} = Typography;
const routes = [
{
path: 'index',
breadcrumbName: 'store',
},
{
path: 'first',
breadcrumbName: 'Dashboard',
},
{
path: 'second',
breadcrumbName: 'Apps',
},
];
const mapStateToProps = state => {
return {
release: state.release,
releaseLoading: state.loadingState.release
}
};
const mapDispatchToProps = dispatch => ({
getRelease: (uuid) => dispatch(getRelease(uuid)),
setLoading: (stateToLoad) => dispatch(setLoading(stateToLoad))
});
class ConnectedRelease extends React.Component {
routes;
constructor(props) {
super(props);
this.routes = props.routes;
}
componentDidMount() {
const {uuid} = this.props.match.params;
this.props.setLoading("release");
this.props.getRelease(uuid);
}
render() {
const release = this.props.release;
let content = <Title level={3}>No Releases Found</Title>;
if (release != null) {
content = <ReleaseView release={release}/>;
}
return (
<div style={{background: '#f0f2f5', padding: 24, minHeight: 780}}>
<Row style={{padding: 10}}>
<Col lg={4}>
</Col>
<Col lg={16} md={24} style={{padding: 3}}>
<Card>
<Skeleton loading={this.props.releaseLoading} avatar={{size: 'large'}} active paragraph={{rows: 8}}>
{content}
</Skeleton>
</Card>
</Col>
</Row>
</div>
);
// //todo remove uppercase
// return (
// <div>
// <div className="main-container">
// <Row style={{padding: 10}}>
// <Col lg={4}>
//
// </Col>
// <Col lg={16} md={24} style={{padding: 3}}>
// <Card>
// <ReleaseView release={release}/>
// </Card>
// </Col>
// </Row>
// </div>
// </div>
//
// );
}
}
const Release = connect(mapStateToProps, mapDispatchToProps)(ConnectedRelease);
export default Release;

@ -21,6 +21,9 @@ const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const config = {
devtool: "source-map",
output: {
publicPath: '/store/' // <---- this
},
watch: false,
resolve: {
alias: {

Loading…
Cancel
Save