474 lines
13 KiB
JavaScript
474 lines
13 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, {
|
|
per_page: "all"
|
|
})
|
|
}
|
|
|
|
// 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.props.fetchimportState()
|
|
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} />
|
|
</Row>
|
|
|
|
<TheForm
|
|
{...this.props}
|
|
fetchimportState={this.fetchimportState}
|
|
lists={
|
|
this.props.data[cs.ModelLists].hasOwnProperty("results")
|
|
? this.props.data[cs.ModelLists].results
|
|
: []
|
|
}
|
|
/>
|
|
|
|
<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 subscribers. The CSV file should have the following headers
|
|
with the exact column names. <code>attributes</code> (optional)
|
|
should be a valid JSON string with double escaped 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
|