import React from "react" import { Link } from "react-router-dom" import { Row, Col, Button, Table, Icon, Tooltip, Tag, Popconfirm, Progress, Modal, notification, Input } from "antd" import dayjs from "dayjs" import relativeTime from "dayjs/plugin/relativeTime" import ModalPreview from "./ModalPreview" import * as cs from "./constants" class Campaigns extends React.PureComponent { defaultPerPage = 20 state = { formType: null, pollID: -1, queryParams: {}, stats: {}, record: null, previewRecord: null, cloneName: "", cloneModalVisible: false, modalWaiting: false } // Pagination config. paginationOptions = { hideOnSinglePage: false, showSizeChanger: true, showQuickJumper: true, defaultPageSize: this.defaultPerPage, pageSizeOptions: ["20", "50", "70", "100"], position: "both", showTotal: (total, range) => `${range[0]} to ${range[1]} of ${total}` } constructor(props) { super(props) this.columns = [ { title: "Name", dataIndex: "name", sorter: true, width: "20%", vAlign: "top", filterIcon: filtered => ( ), filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
{ this.searchInput = node }} placeholder={`Search`} onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : []) } onPressEnter={() => confirm()} style={{ width: 188, marginBottom: 8, display: "block" }} />
), render: (text, record) => { const out = [] out.push(
{text}
{record.subject}
) if (record.tags.length > 0) { for (let i = 0; i < record.tags.length; i++) { out.push({record.tags[i]}) } } return out } }, { title: "Status", dataIndex: "status", className: "status", width: "10%", filters: [ { text: "Draft", value: "draft" }, { text: "Running", value: "running" }, { text: "Scheduled", value: "scheduled" }, { text: "Paused", value: "paused" }, { text: "Cancelled", value: "cancelled" }, { text: "Finished", value: "finished" } ], render: (status, record) => { let color = cs.CampaignStatusColors.hasOwnProperty(status) ? cs.CampaignStatusColors[status] : "" return (
{status} {record.send_at && ( Scheduled —{" "} {dayjs(record.send_at).format(cs.DateFormat)} )}
) } }, { title: "Lists", dataIndex: "lists", width: "25%", align: "left", className: "lists", render: (lists, record) => { const out = [] lists.forEach(l => { out.push( {l.name} ) }) return out } }, { title: "Stats", className: "stats", width: "30%", render: (text, record) => { if ( record.status !== cs.CampaignStatusDraft && record.status !== cs.CampaignStatusScheduled ) { return this.renderStats(record) } } }, { title: "", dataIndex: "actions", className: "actions", width: "15%", render: (text, record) => { return (
{record.status === cs.CampaignStatusPaused && ( this.handleUpdateStatus(record, cs.CampaignStatusRunning) } > )} {record.status === cs.CampaignStatusRunning && ( this.handleUpdateStatus(record, cs.CampaignStatusPaused) } > )} {/* Draft with send_at */} {record.status === cs.CampaignStatusDraft && record.send_at && ( this.handleUpdateStatus(record, cs.CampaignStatusScheduled) } > )} {record.status === cs.CampaignStatusDraft && !record.send_at && ( this.handleUpdateStatus(record, cs.CampaignStatusRunning) } > )} {(record.status === cs.CampaignStatusPaused || record.status === cs.CampaignStatusRunning) && ( this.handleUpdateStatus(record, cs.CampaignStatusCancelled) } > )} { this.handlePreview(record) }} > { let r = { ...record, lists: record.lists.map(i => { return i.id }) } this.handleToggleCloneForm(r) }} > {(record.status === cs.CampaignStatusDraft || record.status === cs.CampaignStatusScheduled) && ( this.handleDeleteRecord(record)} > )}
) } } ] } progressPercent(record) { return Math.round( (this.getStatsField("sent", record) / this.getStatsField("to_send", record)) * 100, 2 ) } isDone(record) { return ( this.getStatsField("status", record) === cs.CampaignStatusFinished || this.getStatsField("status", record) === cs.CampaignStatusCancelled ) } // getStatsField returns a stats field value of a given record if it // exists in the stats state, or the value from the record itself. getStatsField = (field, record) => { if (this.state.stats.hasOwnProperty(record.id)) { return this.state.stats[record.id][field] } return record[field] } renderStats = record => { let color = cs.CampaignStatusColors.hasOwnProperty(record.status) ? cs.CampaignStatusColors[record.status] : "" const startedAt = this.getStatsField("started_at", record) const updatedAt = this.getStatsField("updated_at", record) const sent = this.getStatsField("sent", record) const toSend = this.getStatsField("to_send", record) const isDone = this.isDone(record) const r = this.getStatsField("rate", record) const rate = r ? r : 0 return (
{!isDone && ( )} Sent {sent >= toSend && {toSend}} {sent < toSend && ( {sent} / {toSend} )}   {record.status === cs.CampaignStatusRunning && ( )} {rate > 0 && ( Rate {Math.round(rate, 2)} / min )} Views {record.views} Clicks {record.clicks}
Created {dayjs(record.created_at).format(cs.DateFormat)} {startedAt && ( Started {dayjs(startedAt).format(cs.DateFormat)} )} {isDone && ( Ended {dayjs(updatedAt).format(cs.DateFormat)} )} {startedAt && updatedAt && ( Duration {dayjs(updatedAt).from(dayjs(startedAt), true)} )}
) } componentDidMount() { this.props.pageTitle("Campaigns") dayjs.extend(relativeTime) this.fetchRecords() // Did we land here to start a campaign? let loc = this.props.route.location let state = loc.state if (state && state.hasOwnProperty("campaign")) { this.handleUpdateStatus(state.campaign, state.campaignStatus) delete state.campaign delete state.campaignStatus this.props.route.history.replace({ ...loc, state }) } } componentWillUnmount() { window.clearInterval(this.state.pollID) } fetchRecords = params => { if (!params) { params = {} } let qParams = { page: this.state.queryParams.page, per_page: this.state.queryParams.per_page } // Avoid sending blank string where the enum check will fail. if (!params.status) { delete params.status } if (params) { qParams = { ...qParams, ...params } } this.props .modelRequest( cs.ModelCampaigns, cs.Routes.GetCampaigns, cs.MethodGet, qParams ) .then(r => { this.setState({ queryParams: { ...this.state.queryParams, total: this.props.data[cs.ModelCampaigns].total, per_page: this.props.data[cs.ModelCampaigns].per_page, page: this.props.data[cs.ModelCampaigns].page, query: this.props.data[cs.ModelCampaigns].query, status: params.status } }) this.startStatsPoll() }) } startStatsPoll = () => { window.clearInterval(this.state.pollID) this.setState({ stats: {} }) // If there's at least one running campaign, start polling. let hasRunning = false this.props.data[cs.ModelCampaigns].results.forEach(c => { if (c.status === cs.CampaignStatusRunning) { hasRunning = true return } }) if (!hasRunning) { return } // Poll for campaign stats. let pollID = window.setInterval(() => { this.props .request(cs.Routes.GetRunningCampaignStats, cs.MethodGet) .then(r => { // No more running campaigns. if (r.data.data.length === 0) { window.clearInterval(this.state.pollID) this.fetchRecords() return } let stats = {} r.data.data.forEach(s => { stats[s.id] = s }) this.setState({ stats: stats }) }) .catch(e => { console.log(e.message) }) }, 3000) this.setState({ pollID: pollID }) } handleUpdateStatus = (record, status) => { this.props .modelRequest( cs.ModelCampaigns, cs.Routes.UpdateCampaignStatus, cs.MethodPut, { id: record.id, status: status } ) .then(() => { notification["success"]({ placement: cs.MsgPosition, message: `Campaign ${status}`, description: `"${record.name}" ${status}` }) // Reload the table. this.fetchRecords() }) .catch(e => { notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message }) }) } handleDeleteRecord = record => { this.props .modelRequest( cs.ModelCampaigns, cs.Routes.DeleteCampaign, cs.MethodDelete, { id: record.id } ) .then(() => { notification["success"]({ placement: cs.MsgPosition, message: "Campaign deleted", description: `"${record.name}" deleted` }) // Reload the table. this.fetchRecords() }) .catch(e => { notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message }) }) } handleToggleCloneForm = record => { this.setState({ cloneModalVisible: !this.state.cloneModalVisible, record: record, cloneName: record.name }) } handleCloneCampaign = record => { this.setState({ modalWaiting: true }) this.props .modelRequest( cs.ModelCampaigns, cs.Routes.CreateCampaign, cs.MethodPost, record ) .then(resp => { notification["success"]({ placement: cs.MsgPosition, message: "Campaign created", description: `${record.name} created` }) this.setState({ record: null, modalWaiting: false }) this.props.route.history.push( cs.Routes.ViewCampaign.replace(":id", resp.data.data.id) ) }) .catch(e => { notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message }) this.setState({ modalWaiting: false }) }) } handlePreview = record => { this.setState({ previewRecord: record }) } render() { const pagination = { ...this.paginationOptions, ...this.state.queryParams } return (

Campaigns


record.uuid} dataSource={(() => { if ( !this.props.data[cs.ModelCampaigns] || !this.props.data[cs.ModelCampaigns].hasOwnProperty("results") ) { return [] } return this.props.data[cs.ModelCampaigns].results })()} loading={this.props.reqStates[cs.ModelCampaigns] !== cs.StateDone} pagination={pagination} onChange={(pagination, filters, sorter, records) => { this.fetchRecords({ per_page: pagination.pageSize, page: pagination.current, status: filters.status && filters.status.length > 0 ? filters.status : "", query: filters.name && filters.name.length > 0 ? filters.name[0] : "" }) }} /> {this.state.previewRecord && ( { this.setState({ previewRecord: null }) }} /> )} {this.state.cloneModalVisible && this.state.record && ( { this.handleCloneCampaign({ ...this.state.record, name: this.state.cloneName }) }} > { this.setState({ cloneName: e.target.value }) }} /> )} ) } } export default Campaigns