368 lines
15 KiB
JavaScript
368 lines
15 KiB
JavaScript
import React from "react"
|
|
import { Row, Col, Form, Select, Input, Upload, Button, Radio, Icon, Spin, Progress, Popconfirm, Tag, notification } from "antd"
|
|
import * as cs from "./constants"
|
|
|
|
const StatusNone = "none"
|
|
const StatusImporting = "importing"
|
|
const StatusStopping = "stopping"
|
|
const StatusFinished = "finished"
|
|
const StatusFailed = "failed"
|
|
|
|
class TheFormDef extends React.PureComponent {
|
|
state = {
|
|
confirmDirty: false,
|
|
fileList: [],
|
|
formLoading: false,
|
|
mode: "subscribe"
|
|
}
|
|
|
|
componentDidMount() {
|
|
// Fetch lists.
|
|
this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet)
|
|
}
|
|
|
|
// Handle create / edit form submission.
|
|
handleSubmit = (e) => {
|
|
e.preventDefault()
|
|
var err = null, values = {}
|
|
this.props.form.validateFields((e, v) => {
|
|
err = e
|
|
values = v
|
|
})
|
|
if (err) {
|
|
return
|
|
}
|
|
|
|
if(this.state.fileList.length < 1) {
|
|
notification["error"]({ placement: cs.MsgPosition,
|
|
message: "Error",
|
|
description: "Select a valid file to upload" })
|
|
return
|
|
}
|
|
|
|
this.setState({ formLoading: true })
|
|
let params = new FormData()
|
|
params.set("params", JSON.stringify(values))
|
|
params.append("file", this.state.fileList[0])
|
|
this.props.request(cs.Routes.UploadRouteImport, cs.MethodPost, params).then(() => {
|
|
notification["info"]({ placement: cs.MsgPosition,
|
|
message: "File uploaded",
|
|
description: "Please wait while the import is running" })
|
|
this.props.fetchimportState()
|
|
this.setState({ formLoading: false })
|
|
}).catch(e => {
|
|
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
|
this.setState({ formLoading: false })
|
|
})
|
|
}
|
|
|
|
handleConfirmBlur = (e) => {
|
|
const value = e.target.value
|
|
this.setState({ confirmDirty: this.state.confirmDirty || !!value })
|
|
}
|
|
|
|
onFileChange = (f) => {
|
|
let fileList = [f]
|
|
this.setState({ fileList })
|
|
return false
|
|
}
|
|
|
|
render() {
|
|
const { getFieldDecorator } = this.props.form
|
|
|
|
const formItemLayout = {
|
|
labelCol: { xs: { span: 16 }, sm: { span: 4 } },
|
|
wrapperCol: { xs: { span: 16 }, sm: { span: 10 } }
|
|
}
|
|
|
|
const formItemTailLayout = {
|
|
wrapperCol: { xs: { span: 24, offset: 0 }, sm: { span: 10, offset: 4 } }
|
|
}
|
|
|
|
return (
|
|
<Spin spinning={ this.state.formLoading }>
|
|
<Form onSubmit={this.handleSubmit}>
|
|
<Form.Item {...formItemLayout} label="Mode">
|
|
{getFieldDecorator("mode", { rules: [{ required: true }], initialValue: "subscribe" })(
|
|
<Radio.Group className="mode" onChange={(e) => { this.setState({ mode: e.target.value }) }}>
|
|
<Radio disabled={ this.props.formDisabled } value="subscribe">Subscribe</Radio>
|
|
<Radio disabled={ this.props.formDisabled } value="blacklist">Blacklist</Radio>
|
|
</Radio.Group>
|
|
)}
|
|
</Form.Item>
|
|
{ this.state.mode === "subscribe" &&
|
|
<React.Fragment>
|
|
<Form.Item {...formItemLayout} label="Lists" extra="Lists to subscribe to">
|
|
{getFieldDecorator("lists", { rules: [{ required: true }] })(
|
|
<Select mode="multiple">
|
|
{[...this.props.lists].map((v, i) =>
|
|
<Select.Option value={v["id"]} key={v["id"]}>{v["name"]}</Select.Option>
|
|
)}
|
|
</Select>
|
|
)}
|
|
</Form.Item>
|
|
</React.Fragment>
|
|
}
|
|
{ this.state.mode === "blacklist" &&
|
|
<Form.Item {...formItemTailLayout}>
|
|
<p className="ant-form-extra">
|
|
All existing subscribers found in the import will be marked as 'blacklisted' and will be
|
|
unsubscribed from their existing subscriptions. New subscribers will be imported and marked as 'blacklisted'.
|
|
</p>
|
|
</Form.Item>
|
|
}
|
|
<Form.Item {...formItemLayout} label="CSV column delimiter" extra="Default delimiter is comma">
|
|
{getFieldDecorator("delim", {
|
|
initialValue: ","
|
|
})(<Input maxLength="1" style={{ maxWidth: 40 }} />)}
|
|
</Form.Item>
|
|
<Form.Item
|
|
{...formItemLayout}
|
|
label="CSV or ZIP file">
|
|
<div className="dropbox">
|
|
{getFieldDecorator("file", {
|
|
valuePropName: "file",
|
|
getValueFromEvent: this.normFile,
|
|
rules: [{ required: true }]
|
|
})(
|
|
<Upload.Dragger name="files"
|
|
multiple={ false }
|
|
fileList={ this.state.fileList }
|
|
beforeUpload={ this.onFileChange }
|
|
accept=".zip,.csv">
|
|
<p className="ant-upload-drag-icon">
|
|
<Icon type="inbox" />
|
|
</p>
|
|
<p className="ant-upload-text">Click or drag a CSV or ZIP file here</p>
|
|
</Upload.Dragger>
|
|
)}
|
|
</div>
|
|
</Form.Item>
|
|
<Form.Item {...formItemTailLayout}>
|
|
<p className="ant-form-extra">For existing subscribers, the names and attributes will be overwritten with the values in the CSV.</p>
|
|
<Button type="primary" htmlType="submit"><Icon type="upload" /> Upload</Button>
|
|
</Form.Item>
|
|
</Form>
|
|
</Spin>
|
|
)
|
|
}
|
|
}
|
|
const TheForm = Form.create()(TheFormDef)
|
|
|
|
class Importing extends React.PureComponent {
|
|
state = {
|
|
pollID: -1,
|
|
logs: ""
|
|
}
|
|
|
|
stopImport = () => {
|
|
// Get the import status.
|
|
this.props.request(cs.Routes.UploadRouteImport, cs.MethodDelete).then((r) => {
|
|
this.props.fetchimportState()
|
|
}).catch(e => {
|
|
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
|
})
|
|
}
|
|
|
|
componentDidMount() {
|
|
// Poll for stats until it's finished or failed.
|
|
let pollID = window.setInterval(() => {
|
|
this.props.fetchimportState()
|
|
this.fetchLogs()
|
|
if( this.props.importState.status === StatusFinished ||
|
|
this.props.importState.status === StatusFailed ) {
|
|
window.clearInterval(this.state.pollID)
|
|
}
|
|
}, 1000)
|
|
|
|
this.setState({ pollID: pollID })
|
|
}
|
|
componentWillUnmount() {
|
|
window.clearInterval(this.state.pollID)
|
|
}
|
|
|
|
fetchLogs() {
|
|
this.props.request(cs.Routes.GetRouteImportLogs, cs.MethodGet).then((r) => {
|
|
this.setState({ logs: r.data.data })
|
|
let t = document.querySelector("#log-textarea")
|
|
t.scrollTop = t.scrollHeight;
|
|
}).catch(e => {
|
|
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
|
})
|
|
}
|
|
|
|
render() {
|
|
let progressPercent = 0
|
|
if( this.props.importState.status === StatusFinished ) {
|
|
progressPercent = 100
|
|
} else {
|
|
progressPercent = Math.floor(this.props.importState.imported / this.props.importState.total * 100)
|
|
}
|
|
|
|
return(
|
|
<section className="content import">
|
|
<h1>Importing — { this.props.importState.name }</h1>
|
|
{ this.props.importState.status === StatusImporting &&
|
|
<p>Import is in progress. It is safe to navigate away from this page.</p>
|
|
}
|
|
|
|
{ this.props.importState.status !== StatusImporting &&
|
|
<p>Import has finished.</p>
|
|
}
|
|
|
|
<Row className="import-container">
|
|
<Col span="10" offset="3">
|
|
<div className="stats center">
|
|
<div>
|
|
<Progress type="line" percent={ progressPercent } />
|
|
</div>
|
|
|
|
<div>
|
|
<h3>{ this.props.importState.imported } records</h3>
|
|
<br />
|
|
|
|
{ this.props.importState.status === StatusImporting &&
|
|
<Popconfirm title="Are you sure?" onConfirm={() => this.stopImport()}>
|
|
<p><Icon type="loading" /></p>
|
|
<Button type="primary">Stop import</Button>
|
|
</Popconfirm>
|
|
}
|
|
{ this.props.importState.status === StatusStopping &&
|
|
<div>
|
|
<p><Icon type="loading" /></p>
|
|
<h4>Stopping</h4>
|
|
</div>
|
|
}
|
|
{ this.props.importState.status !== StatusImporting &&
|
|
this.props.importState.status !== StatusStopping &&
|
|
<div>
|
|
{ this.props.importState.status !== StatusFinished &&
|
|
<div>
|
|
<Tag color="red">{ this.props.importState.status }</Tag>
|
|
<br />
|
|
</div>
|
|
}
|
|
|
|
<br />
|
|
<Button type="primary" onClick={() => this.stopImport()}>Done</Button>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="logs">
|
|
<h3>Import log</h3>
|
|
<Spin spinning={ this.state.logs === "" }>
|
|
<Input.TextArea placeholder="Import logs"
|
|
id="log-textarea"
|
|
rows={10}
|
|
value={ this.state.logs }
|
|
autosize={{ minRows: 2, maxRows: 10 }} />
|
|
</Spin>
|
|
</div>
|
|
</Col>
|
|
</Row>
|
|
</section>
|
|
)
|
|
}
|
|
}
|
|
|
|
class Import extends React.PureComponent {
|
|
state = {
|
|
importState: { "status": "" }
|
|
}
|
|
|
|
fetchimportState = () => {
|
|
// Get the import status.
|
|
this.props.request(cs.Routes.GetRouteImportStats, cs.MethodGet).then((r) => {
|
|
this.setState({ importState: r.data.data })
|
|
}).catch(e => {
|
|
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
|
})
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.props.pageTitle("Import subscribers")
|
|
this.fetchimportState()
|
|
}
|
|
render() {
|
|
if( this.state.importState.status === "" ) {
|
|
// Fetching the status.
|
|
return (
|
|
<section className="content center">
|
|
<Spin />
|
|
</section>
|
|
)
|
|
} else if ( this.state.importState.status !== StatusNone ) {
|
|
// There's an import state
|
|
return <Importing { ...this.props }
|
|
importState={ this.state.importState }
|
|
fetchimportState={ this.fetchimportState } />
|
|
}
|
|
|
|
return (
|
|
<section className="content import">
|
|
<Row>
|
|
<Col span={22}><h1>Import subscribers</h1></Col>
|
|
<Col span={2}>
|
|
</Col>
|
|
</Row>
|
|
|
|
<TheForm { ...this.props }
|
|
fetchimportState={ this.fetchimportState }
|
|
lists={ this.props.data[cs.ModelLists] }>
|
|
</TheForm>
|
|
|
|
<hr />
|
|
<div className="help">
|
|
<h2>Instructions</h2>
|
|
<p>Upload a CSV file or a ZIP file with a single CSV file in it
|
|
to bulk import a subscribers.
|
|
</p>
|
|
<p>
|
|
The CSV file should have the following headers with the exact column names
|
|
(<code>status</code> and <code>attributes</code> are optional).
|
|
{" "}
|
|
<code>attributes</code> should be a valid JSON string with double escaped quotes.
|
|
Spreadsheet programs should automatically take care of this without having you manually
|
|
escape quotes.
|
|
</p>
|
|
|
|
<blockquote className="csv-example">
|
|
<code className="csv-headers">
|
|
<span>email,</span>
|
|
<span>name,</span>
|
|
<span>status,</span>
|
|
<span>attributes</span>
|
|
</code>
|
|
</blockquote>
|
|
|
|
<h3>Example raw CSV</h3>
|
|
<blockquote className="csv-example">
|
|
<code className="csv-headers">
|
|
<span>email,</span>
|
|
<span>name,</span>
|
|
<span>status,</span>
|
|
<span>attributes</span>
|
|
</code>
|
|
<code className="csv-row">
|
|
<span>user1@mail.com,</span>
|
|
<span>"User One",</span>
|
|
<span>enabled,</span>
|
|
<span>{ '"{""age"": 32, ""city"": ""Bangalore""}"' }</span>
|
|
</code>
|
|
<code className="csv-row">
|
|
<span>user2@mail.com,</span>
|
|
<span>"User Two",</span>
|
|
<span>blacklisted,</span>
|
|
<span>{ '"{""age"": 25, ""occupation"": ""Time Traveller""}"' }</span>
|
|
</code>
|
|
</blockquote>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
}
|
|
|
|
export default Import
|