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

Improve APIM's edit release view and fix install/uninstall's timestamp issue

## Purpose
The purpose is to fix the following issues with this PR.
1. In the current APPM Publisher UI's edit release section, there is no way to preview selected images. Also, the supported versions are hardcoded.
2. There is an issue after adding a scheduled install/uninstall feature to the UI, which tries to send a query param with null timestamp value with non-scheduled operations.

## Goals
- Add previews of selected images to upload
- Add supported OS versions field
- Fix timestamp issue in scheduled install/uninstall operation in APPM UI

## Approach
- Used the same approach to add image previews and supported OS versions field which used in the "Add New Release" view in APPM publisher UI.
- Removed the inline function which caused the timestamp issue & created a new function to handle the operation.

## Documentation
> N/A

## Automation tests
> N/A

## Security checks
 - Followed secure coding standards? (yes)
 - Ran FindSecurityBugs plugin and verified report? (no)
 - Confirmed that this PR doesn't commit any keys, passwords, tokens, usernames, or other secrets? (yes)

## Related MRs
> (Any other related Merge Requests)

## Test environment
> (Context and environment where development was performed)

## Learning
> (Any additional learning resources)

See merge request entgra/carbon-device-mgt!295
feature/appm-store/pbac
Saad Sahibjan 5 years ago
commit 11c4d15518

@ -28,6 +28,13 @@ import {withConfigContext} from "../../../context/ConfigContext";
const {Title, Text, Paragraph} = Typography; const {Title, Text, Paragraph} = Typography;
class ReleaseView extends React.Component { class ReleaseView extends React.Component {
constructor(props) {
super(props);
this.state = {
}
}
render() { render() {
const {app, release} = this.props; const {app, release} = this.props;
const config = this.props.context; const config = this.props.context;
@ -88,8 +95,10 @@ class ReleaseView extends React.Component {
<EditRelease <EditRelease
isAppUpdatable={isAppUpdatable} isAppUpdatable={isAppUpdatable}
type={app.type} type={app.type}
deviceType={app.deviceType}
release={release} release={release}
updateRelease={this.props.updateRelease} updateRelease={this.props.updateRelease}
supportedOsVersions={[...this.props.supportedOsVersions]}
/> />
</Col> </Col>

@ -17,12 +17,28 @@
*/ */
import React from "react"; import React from "react";
import {Modal, Button, Icon, notification, Spin, Tooltip, Upload, Input, Switch, Form, Divider, Row, Col} from 'antd'; import {
Modal,
Button,
Icon,
notification,
Spin,
Tooltip,
Upload,
Input,
Switch,
Form,
Divider,
Row,
Col,
Select
} from 'antd';
import axios from "axios"; import axios from "axios";
import {withConfigContext} from "../../../../context/ConfigContext"; import {withConfigContext} from "../../../../context/ConfigContext";
const {TextArea} = Input; const {TextArea} = Input;
const InputGroup = Input.Group; const InputGroup = Input.Group;
const {Option} = Select;
const formItemLayout = { const formItemLayout = {
labelCol: { labelCol: {
@ -33,6 +49,15 @@ const formItemLayout = {
}, },
}; };
function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
class EditReleaseModal extends React.Component { class EditReleaseModal extends React.Component {
constructor(props) { constructor(props) {
@ -51,6 +76,8 @@ class EditReleaseModal extends React.Component {
specificElements: {} specificElements: {}
} }
}; };
this.lowerOsVersion = null;
this.upperOsVersion = null;
} }
componentDidMount = () => { componentDidMount = () => {
@ -117,6 +144,7 @@ class EditReleaseModal extends React.Component {
showModal = () => { showModal = () => {
const config = this.props.context;
const {app, release} = this.props; const {app, release} = this.props;
const {formConfig} = this.state; const {formConfig} = this.state;
const {specificElements} = formConfig; const {specificElements} = formConfig;
@ -143,14 +171,19 @@ class EditReleaseModal extends React.Component {
} }
}); });
// if (specificElements.hasOwnProperty("packageName")) { if ((config.deviceTypes.mobileTypes.includes(this.props.deviceType))) {
// this.props.form.setFields({ const osVersions = release.supportedOsVersions.split("-");
// packageName: { this.lowerOsVersion = osVersions[0];
// value: app.packageName this.upperOsVersion = osVersions[1];
// } this.props.form.setFields({
// }); lowerOsVersion: {
// } value: osVersions[0]
},
upperOsVersion: {
value: osVersions[1]
}
});
}
if (specificElements.hasOwnProperty("version")) { if (specificElements.hasOwnProperty("version")) {
this.props.form.setFields({ this.props.form.setFields({
version: { version: {
@ -232,9 +265,12 @@ class EditReleaseModal extends React.Component {
isSharedWithAllTenants, isSharedWithAllTenants,
metaData: JSON.stringify(this.state.metaData), metaData: JSON.stringify(this.state.metaData),
releaseType: releaseType, releaseType: releaseType,
supportedOsVersions: "4-30"
}; };
if ((config.deviceTypes.mobileTypes.includes(this.props.deviceType))) {
release.supportedOsVersions = `${this.lowerOsVersion}-${this.upperOsVersion}`;
}
if (specificElements.hasOwnProperty("binaryFile") && binaryFiles.length === 1) { if (specificElements.hasOwnProperty("binaryFile") && binaryFiles.length === 1) {
data.append('binaryFile', binaryFiles[0].originFileObj); data.append('binaryFile', binaryFiles[0].originFileObj);
} }
@ -322,10 +358,50 @@ class EditReleaseModal extends React.Component {
}) })
}; };
handlePreviewCancel = () => this.setState({previewVisible: false});
handlePreview = async file => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
this.setState({
previewImage: file.url || file.preview,
previewVisible: true,
});
};
handleLowerOsVersionChange = (lowerOsVersion) => {
this.lowerOsVersion = lowerOsVersion;
};
handleUpperOsVersionChange = (upperOsVersion) => {
this.upperOsVersion = upperOsVersion;
};
render() { render() {
const {formConfig, icons, screenshots, loading, binaryFiles, metaData} = this.state; const {
formConfig,
icons,
screenshots,
loading,
binaryFiles,
metaData,
previewImage,
previewVisible,
binaryFileHelperText,
iconHelperText,
screenshotHelperText
} = this.state;
const {getFieldDecorator} = this.props.form; const {getFieldDecorator} = this.props.form;
const {isAppUpdatable} = this.props; const {isAppUpdatable, supportedOsVersions, deviceType} = this.props;
const config = this.props.context;
const uploadButton = (
<div>
<Icon type="plus"/>
<div className="ant-upload-text">Select</div>
</div>
);
return ( return (
<div> <div>
@ -340,8 +416,8 @@ class EditReleaseModal extends React.Component {
title="Edit release" title="Edit release"
visible={this.state.visible} visible={this.state.visible}
footer={null} footer={null}
onCancel={this.handleCancel} width={580}
> onCancel={this.handleCancel}>
<div> <div>
<Spin tip="Uploading..." spinning={loading}> <Spin tip="Uploading..." spinning={loading}>
<Form labelAlign="left" layout="horizontal" <Form labelAlign="left" layout="horizontal"
@ -370,19 +446,6 @@ class EditReleaseModal extends React.Component {
</Form.Item> </Form.Item>
)} )}
{/*{formConfig.specificElements.hasOwnProperty("packageName") && (*/}
{/* <Form.Item {...formItemLayout} label="Package Name">*/}
{/* {getFieldDecorator('packageName', {*/}
{/* rules: [{*/}
{/* required: true,*/}
{/* message: 'Please input the package name'*/}
{/* }],*/}
{/* })(*/}
{/* <Input placeholder="Package Name"/>*/}
{/* )}*/}
{/* </Form.Item>*/}
{/*)}*/}
{formConfig.specificElements.hasOwnProperty("url") && ( {formConfig.specificElements.hasOwnProperty("url") && (
<Form.Item {...formItemLayout} label="URL"> <Form.Item {...formItemLayout} label="URL">
{getFieldDecorator('url', { {getFieldDecorator('url', {
@ -418,19 +481,15 @@ class EditReleaseModal extends React.Component {
})( })(
<Upload <Upload
name="logo" name="logo"
listType="picture-card"
onChange={this.handleIconChange} onChange={this.handleIconChange}
beforeUpload={() => false} beforeUpload={() => false}
> onPreview={this.handlePreview}>
{icons.length !== 1 && ( {icons.length === 1 ? null : uploadButton}
<Button>
<Icon type="upload"/> Change
</Button>
)}
</Upload>, </Upload>,
)} )}
</Form.Item> </Form.Item>
<Form.Item {...formItemLayout} label="Screenshots"> <Form.Item {...formItemLayout} label="Screenshots">
{getFieldDecorator('screenshots', { {getFieldDecorator('screenshots', {
valuePropName: 'icon', valuePropName: 'icon',
@ -440,15 +499,11 @@ class EditReleaseModal extends React.Component {
})( })(
<Upload <Upload
name="screenshots" name="screenshots"
listType="picture-card"
onChange={this.handleScreenshotChange} onChange={this.handleScreenshotChange}
beforeUpload={() => false} beforeUpload={() => false}
multiple onPreview={this.handlePreview}>
> {screenshots.length >= 3 ? null : uploadButton}
{screenshots.length < 3 && (
<Button>
<Icon type="upload"/> Click to upload
</Button>
)}
</Upload>, </Upload>,
)} )}
</Form.Item> </Form.Item>
@ -475,7 +530,65 @@ class EditReleaseModal extends React.Component {
rows={5}/> rows={5}/>
)} )}
</Form.Item> </Form.Item>
{(config.deviceTypes.mobileTypes.includes(deviceType)) && (
<Form.Item {...formItemLayout} label="Supported OS Versions">
{getFieldDecorator('supportedOS')(
<div>
<InputGroup>
<Row gutter={8}>
<Col span={11}>
<Form.Item>
{getFieldDecorator('lowerOsVersion', {
rules: [{
required: true,
message: 'Please select Value'
}],
})(
<Select
placeholder="Lower version"
style={{width: "100%"}}
onChange={this.handleLowerOsVersionChange}>
{supportedOsVersions.map(version => (
<Option key={version.versionName}
value={version.versionName}>
{version.versionName}
</Option>
))}
</Select>
)}
</Form.Item>
</Col>
<Col span={2}>
<p> - </p>
</Col>
<Col span={11}>
<Form.Item>
{getFieldDecorator('upperOsVersion', {
rules: [{
required: true,
message: 'Please select Value'
}],
})(
<Select style={{width: "100%"}}
placeholder="Upper version"
onChange={this.handleUpperOsVersionChange}>
{supportedOsVersions.map(version => (
<Option key={version.versionName}
value={version.versionName}>
{version.versionName}
</Option>
))}
</Select>
)}
</Form.Item>
</Col>
</Row>
</InputGroup>
</div>
)}
</Form.Item>
)}
<Form.Item {...formItemLayout} label="Price"> <Form.Item {...formItemLayout} label="Price">
{getFieldDecorator('price', { {getFieldDecorator('price', {
rules: [{ rules: [{
@ -575,6 +688,9 @@ class EditReleaseModal extends React.Component {
</Form> </Form>
</Spin> </Spin>
</div> </div>
<Modal visible={previewVisible} footer={null} onCancel={this.handlePreviewCancel}>
<img alt="Preview Image" style={{width: '100%'}} src={previewImage}/>
</Modal>
</Modal> </Modal>
</div> </div>
); );

@ -37,7 +37,6 @@ class LifeCycleDetailsModal extends React.Component {
}; };
handleCancel = e => { handleCancel = e => {
console.log(e);
this.setState({ this.setState({
visible: false, visible: false,
}); });

@ -39,7 +39,6 @@ class AddNewPage extends React.Component {
handleCancel = e => { handleCancel = e => {
console.log(e);
this.setState({ this.setState({
visible: false, visible: false,
}); });

@ -37,14 +37,12 @@ class GooglePlayIframe extends React.Component {
}; };
handleOk = e => { handleOk = e => {
console.log(e);
this.setState({ this.setState({
visible: false, visible: false,
}); });
}; };
handleCancel = e => { handleCancel = e => {
console.log(e);
this.setState({ this.setState({
visible: false, visible: false,
}); });

@ -43,14 +43,12 @@ class ManagedConfigurationsIframe extends React.Component {
}; };
handleOk = e => { handleOk = e => {
console.log(e);
this.setState({ this.setState({
visible: false, visible: false,
}); });
}; };
handleCancel = e => { handleCancel = e => {
console.log(e);
this.setState({ this.setState({
visible: false, visible: false,
}); });
@ -116,7 +114,6 @@ class ManagedConfigurationsIframe extends React.Component {
updateConfig = (method, event) => { updateConfig = (method, event) => {
const {packageName} = this.props; const {packageName} = this.props;
this.setState({loading: true}); this.setState({loading: true});
console.log(event);
const data = { const data = {
mcmId: event.mcmId, mcmId: event.mcmId,
@ -151,7 +148,6 @@ class ManagedConfigurationsIframe extends React.Component {
deleteConfig = (event) => { deleteConfig = (event) => {
const {packageName} = this.props; const {packageName} = this.props;
this.setState({loading: true}); this.setState({loading: true});
console.log(event);
//send request to the invoker //send request to the invoker
axios.delete( axios.delete(

@ -47,7 +47,6 @@ class Pages extends React.Component {
rowSelection = { rowSelection = {
onChange: (selectedRowKeys, selectedRows) => { onChange: (selectedRowKeys, selectedRows) => {
// console.lohhhg(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
this.setState({ this.setState({
selectedRows: selectedRows selectedRows: selectedRows
}) })

@ -84,9 +84,6 @@ class NewAppUploadForm extends React.Component {
this.setState({ this.setState({
loading: true loading: true
}); });
console.log(values);
const {price, isSharedWithAllTenants, binaryFile, icon, screenshots, releaseDescription, releaseType} = values; const {price, isSharedWithAllTenants, binaryFile, icon, screenshots, releaseDescription, releaseType} = values;
//add release data //add release data
@ -234,8 +231,7 @@ class NewAppUploadForm extends React.Component {
<Form <Form
labelAlign="right" labelAlign="right"
layout="horizontal" layout="horizontal"
onSubmit={this.handleSubmit} onSubmit={this.handleSubmit}>
>
{formConfig.specificElements.hasOwnProperty("binaryFile") && ( {formConfig.specificElements.hasOwnProperty("binaryFile") && (
<Form.Item {...formItemLayout} <Form.Item {...formItemLayout}
label="Application" label="Application"
@ -250,8 +246,7 @@ class NewAppUploadForm extends React.Component {
<Upload <Upload
name="binaryFile" name="binaryFile"
onChange={this.handleBinaryFileChange} onChange={this.handleBinaryFileChange}
beforeUpload={() => false} beforeUpload={() => false}>
>
{binaryFiles.length !== 1 && ( {binaryFiles.length !== 1 && (
<Button> <Button>
<Icon type="upload"/> Click to upload <Icon type="upload"/> Click to upload
@ -277,8 +272,7 @@ class NewAppUploadForm extends React.Component {
listType="picture-card" listType="picture-card"
onChange={this.handleIconChange} onChange={this.handleIconChange}
beforeUpload={() => false} beforeUpload={() => false}
onPreview={this.handlePreview} onPreview={this.handlePreview}>
>
{icons.length === 1 ? null : uploadButton} {icons.length === 1 ? null : uploadButton}
</Upload>, </Upload>,
)} )}
@ -418,7 +412,6 @@ class NewAppUploadForm extends React.Component {
</Select>, </Select>,
)} )}
</Form.Item> </Form.Item>
<Form.Item {...formItemLayout} label="Price"> <Form.Item {...formItemLayout} label="Price">
{getFieldDecorator('price', { {getFieldDecorator('price', {
rules: [{ rules: [{
@ -437,7 +430,6 @@ class NewAppUploadForm extends React.Component {
/> />
)} )}
</Form.Item> </Form.Item>
<Form.Item {...formItemLayout} label="Is Shared?"> <Form.Item {...formItemLayout} label="Is Shared?">
{getFieldDecorator('isSharedWithAllTenants', { {getFieldDecorator('isSharedWithAllTenants', {
rules: [{ rules: [{
@ -450,7 +442,6 @@ class NewAppUploadForm extends React.Component {
unCheckedChildren={<Icon type="close"/>} unCheckedChildren={<Icon type="close"/>}
/> />
)} )}
</Form.Item> </Form.Item>
<Form.Item {...formItemLayout} label="Meta Data"> <Form.Item {...formItemLayout} label="Meta Data">
{getFieldDecorator('meta', { {getFieldDecorator('meta', {
@ -510,7 +501,6 @@ class NewAppUploadForm extends React.Component {
</Button> </Button>
</div> </div>
)} )}
</Form.Item> </Form.Item>
<Form.Item style={{float: "right", marginLeft: 8}}> <Form.Item style={{float: "right", marginLeft: 8}}>
<Button type="primary" htmlType="submit"> <Button type="primary" htmlType="submit">
@ -531,7 +521,6 @@ class NewAppUploadForm extends React.Component {
</div> </div>
); );
} }
} }
export default (Form.create({name: 'app-upload-form'})(NewAppUploadForm)); export default (Form.create({name: 'app-upload-form'})(NewAppUploadForm));

@ -39,7 +39,8 @@ class Release extends React.Component {
uuid: null, uuid: null,
release: null, release: null,
currentLifecycleStatus: null, currentLifecycleStatus: null,
lifecycle: null lifecycle: null,
supportedOsVersions:[]
}; };
} }
@ -85,14 +86,17 @@ class Release extends React.Component {
loading: false, loading: false,
uuid: uuid uuid: uuid
}); });
if(config.deviceTypes.mobileTypes.includes(app.deviceType)){
this.getSupportedOsVersions(app.deviceType);
}else{
this.getLifecycle();
}
} }
}).catch((error) => { }).catch((error) => {
handleApiError(error, "Error occurred while trying to load the release."); handleApiError(error, "Error occurred while trying to load the release.");
this.setState({loading: false}); this.setState({loading: false});
}); });
this.getLifecycle();
}; };
getLifecycle = () => { getLifecycle = () => {
@ -112,6 +116,28 @@ class Release extends React.Component {
}); });
}; };
getSupportedOsVersions = (deviceType) => {
const config = this.props.context;
axios.get(
window.location.origin + config.serverConfig.invoker.uri + config.serverConfig.invoker.deviceMgt +
`/admin/device-types/${deviceType}/versions`
).then(res => {
if (res.status === 200) {
let supportedOsVersions = JSON.parse(res.data.data);
this.setState({
supportedOsVersions,
loading: false,
});
this.getLifecycle();
}
}).catch((error) => {
handleApiError(error, "Error occurred while trying to load supported OS versions.");
this.setState({
loading: false
});
});
};
render() { render() {
const {app, release, currentLifecycleStatus, lifecycle, loading} = this.state; const {app, release, currentLifecycleStatus, lifecycle, loading} = this.state;
@ -138,6 +164,7 @@ class Release extends React.Component {
currentLifecycleStatus={currentLifecycleStatus} currentLifecycleStatus={currentLifecycleStatus}
lifecycle={lifecycle} lifecycle={lifecycle}
updateRelease={this.updateRelease} updateRelease={this.updateRelease}
supportedOsVersions = {[...this.state.supportedOsVersions]}
/>) />)
} }
</Skeleton> </Skeleton>

@ -62,16 +62,16 @@ class ReleaseView extends React.Component {
headers: {'X-Platform': config.serverConfig.platform} headers: {'X-Platform': config.serverConfig.platform}
} }
).then(res => { ).then(res => {
if (res.status === 200) { if (res.status === 200 || res.status === 201) {
this.setState({ this.setState({
loading: false, loading: false,
appInstallModalVisible: false, appInstallModalVisible: false,
appUnInstallModalVisible: false, appUninstallModalVisible: false,
}); });
notification["success"]({ notification["success"]({
message: 'Done!', message: 'Done!',
description: description:
'App '+operation+'ed triggered.', 'Operation triggered.',
}); });
} else { } else {
this.setState({ this.setState({

@ -193,7 +193,7 @@ class DeviceUninstall extends React.Component {
type: device.type type: device.type
}); });
}); });
this.props.onUninstall("devices", payload, "uninstall", null); this.props.onUninstall("devices", payload, "uninstall", timestamp);
}; };
render() { render() {

@ -88,7 +88,7 @@ class GroupUninstall extends React.Component {
value.map(val=>{ value.map(val=>{
data.push(val.key); data.push(val.key);
}); });
this.props.onUninstall("group", data, "uninstall",null); this.props.onUninstall("group", data, "uninstall",timestamp);
}; };
render() { render() {

@ -87,7 +87,7 @@ class RoleUninstall extends React.Component {
value.map(val=>{ value.map(val=>{
data.push(val.key); data.push(val.key);
}); });
this.props.onUninstall("role", data, "uninstall",null); this.props.onUninstall("role", data, "uninstall",timestamp);
}; };
render() { render() {

@ -86,7 +86,7 @@ class UserUninstall extends React.Component {
value.map(val => { value.map(val => {
data.push(val.key); data.push(val.key);
}); });
this.props.onUninstall("user", data, "uninstall",null); this.props.onUninstall("user", data, "uninstall",timestamp);
}; };
render() { render() {

@ -46,6 +46,14 @@ class InstallModalFooter extends React.Component{
}) })
}; };
triggerInstallOperation = () =>{
this.props.operation();
};
triggerScheduledInstallOperation = () =>{
const {scheduledTime} =this.state;
this.props.operation(scheduledTime);
};
render() { render() {
const {scheduledTime,isScheduledInstallVisible} =this.state; const {scheduledTime,isScheduledInstallVisible} =this.state;
const {disabled, type} = this.props; const {disabled, type} = this.props;
@ -56,7 +64,7 @@ class InstallModalFooter extends React.Component{
display: (!isScheduledInstallVisible)?'block':'none' display: (!isScheduledInstallVisible)?'block':'none'
}}> }}>
<Button style={{margin: 5}} disabled={disabled} htmlType="button" type="primary" <Button style={{margin: 5}} disabled={disabled} htmlType="button" type="primary"
onClick={this.props.operation}> onClick={this.triggerInstallOperation}>
{type} {type}
</Button> </Button>
<Button style={{margin: 5}} disabled={disabled} htmlType="button" <Button style={{margin: 5}} disabled={disabled} htmlType="button"
@ -76,9 +84,7 @@ class InstallModalFooter extends React.Component{
style={{margin: 5}} style={{margin: 5}}
htmlType="button" htmlType="button"
type="primary" type="primary"
onClick={()=>{ onClick={this.triggerScheduledInstallOperation}>
this.props.operation(scheduledTime);
}}>
Schedule Schedule
</Button> </Button>
<Button style={{margin: 5}} htmlType="button" <Button style={{margin: 5}} htmlType="button"

@ -41,7 +41,6 @@ class EditReview extends React.Component {
componentDidMount() { componentDidMount() {
const {content,rating,id} = this.props.review; const {content,rating,id} = this.props.review;
console.log(this.props.review);
this.setState({ this.setState({
content, content,
rating rating

@ -19,7 +19,6 @@
import {notification} from "antd"; import {notification} from "antd";
export const handleApiError = (error, message) => { export const handleApiError = (error, message) => {
console.log(error);
if (error.hasOwnProperty("response") && error.response.status === 401) { if (error.hasOwnProperty("response") && error.response.status === 401) {
const redirectUrl = encodeURI(window.location.href); const redirectUrl = encodeURI(window.location.href);
window.location.href = window.location.origin + `/store/login?redirect=${redirectUrl}`; window.location.href = window.location.origin + `/store/login?redirect=${redirectUrl}`;

@ -80,7 +80,6 @@ class NormalLoginForm extends React.Component {
handleSubmit = (e) => { handleSubmit = (e) => {
const thisForm = this; const thisForm = this;
const config = this.props.context; const config = this.props.context;
console.log(config);
e.preventDefault(); e.preventDefault();
this.props.form.validateFields((err, values) => { this.props.form.validateFields((err, values) => {

Loading…
Cancel
Save