appbaseio/dejaVu

View on GitHub
packages/browser/src/components/DataBrowser/ExportData.js

Summary

Maintainability
F
4 days
Test Coverage
// @flow
import React, { Component, Fragment } from 'react';
import { DownloadOutlined } from '@ant-design/icons';
import { Modal, Button, Spin, Alert, Row, Col, Select, Checkbox } from 'antd';
import { connect } from 'react-redux';
import { unparse } from 'papaparse';
import { saveAs } from 'file-saver';

import { getIndexTypeMap, getVisibleColumns } from '../../reducers/mappings';
import { getUrl } from '../../reducers/app';
import { getVersion } from '../../reducers/version';
import { addDataRequest } from '../../actions';
import exportData, { flatten, MAX_DATA } from '../../utils/exportData';
import { numberWithCommas } from '../../utils';
import { getCount } from '../../apis';
import { getStats } from '../../reducers/stats';
import { getQuery } from '../../reducers/query';
import colors from '../theme/colors';

import Flex from '../Flex';
import Item from './Item.styles';

const { Option } = Select;

const getChunks = (count, maxCount) => {
    const chunks = {};
    const max = maxCount || MAX_DATA;
    if (count < max) {
        chunks[`0-${count}`] = {
            from: 0,
            total: count,
        };
    } else {
        const maxChunks = Math.ceil(count / max);
        Array(maxChunks)
            .fill(maxChunks)
            .forEach((item, index) => {
                const nextChunk = index * max;
                chunks[
                    `${nextChunk + 1}-${
                        index < maxChunks - 1 ? nextChunk + max : count
                    }`
                ] = {
                    total: index < maxChunks - 1 ? max : count - nextChunk,
                };
            });
    }

    return chunks;
};

type Props = {
    url: string,
    version: number,
    indexTypeMap: any,
    stats: any,
    query: any,
    visibleColumns: any,
};

type State = {
    isShowingModal: boolean,
    isDownloading: boolean,
    isFetchingCount: boolean,
    error?: any,
    countChunks: any,
    selectedChunk: string,
    searchAfterData: string,
    selectedIndex: string,
    types: string[],
    selectedType: string,
    applyCurrentQuery: boolean,
    totalCount: number,
};

class ExportData extends Component<Props, State> {
    state = {
        isShowingModal: false,
        isDownloading: false,
        isFetchingCount: false,
        error: null,
        countChunks: {},
        selectedChunk: '',
        searchAfterData: '',
        selectedIndex: Object.keys(this.props.indexTypeMap)[0],
        types: this.props.indexTypeMap[Object.keys(this.props.indexTypeMap)[0]],
        selectedType: this.props.indexTypeMap[
            Object.keys(this.props.indexTypeMap)[0]
        ][0],
        applyCurrentQuery: false,
        totalCount: 0,
    };

    handleAfterClose = () => {
        this.setState({
            error: null,
            countChunks: {},
            isFetchingCount: false,
            selectedChunk: '',
            searchAfterData: '',
            applyCurrentQuery: false,
        });
    };

    fetchData = async () => {
        this.setState({
            isDownloading: true,
        });

        const { url, version, query } = this.props;
        const {
            selectedIndex,
            selectedType,
            selectedChunk,
            countChunks,
            searchAfterData,
            applyCurrentQuery,
        } = this.state;

        const exportQuery = applyCurrentQuery ? { query: query.query } : null;
        try {
            const { data, searchAfter } = await exportData(
                selectedIndex,
                selectedType,
                url,
                version,
                exportQuery,
                countChunks[selectedChunk],
                searchAfterData,
            );

            const chunkList = Object.keys(countChunks);
            const currentIndex = chunkList.findIndex(
                item => item === selectedChunk,
            );

            const hasNextChunk = currentIndex < chunkList.length - 1;

            this.setState({
                isDownloading: false,
                selectedChunk: hasNextChunk
                    ? chunkList[currentIndex + 1]
                    : chunkList[0],
                searchAfterData: hasNextChunk ? `${searchAfter}` : '',
            });

            return data;
        } catch (err) {
            this.setState({
                isDownloading: false,
                error: err,
            });
            return [err];
        }
    };

    fetchCount = async () => {
        this.setState({
            isFetchingCount: true,
        });

        const { url, version } = this.props;
        const { selectedIndex, selectedType } = this.state;

        try {
            const data = await getCount(
                selectedIndex,
                selectedType,
                url,
                version,
            );
            const count = data.count || 0;
            const chunks = getChunks(count);
            this.setState({
                isFetchingCount: false,
                totalCount: count,
                countChunks: chunks,
                selectedChunk: Object.keys(chunks)[0],
            });
        } catch (err) {
            this.setState({
                isFetchingCount: false,
                error: err,
                countChunks: {},
            });
        }
    };

    toggleModal = () => {
        this.setState(
            prevState => ({
                isShowingModal: !prevState.isShowingModal,
            }),
            () => {
                if (this.state.isShowingModal) {
                    this.fetchCount();
                }
            },
        );
    };

    onCSVClick = async () => {
        const {
            selectedChunk,
            selectedIndex,
            selectedType,
            applyCurrentQuery,
        } = this.state;
        const { visibleColumns } = this.props;
        visibleColumns.push('_id');
        const res = await this.fetchData();
        const flattenData = res.map(item => flatten(item));
        const newData = unparse({
            fields: visibleColumns,
            data: flattenData,
        });
        const file = new File(
            [newData],
            `data_${selectedIndex}${
                applyCurrentQuery ? '' : `_${selectedType}`
            }_${selectedChunk.replace(/-/g, '_')}.csv`,
            {
                type: 'text/comma-separated-values;charset=utf-8',
            },
        );
        saveAs(file);
    };

    onJSONClick = async () => {
        const {
            selectedChunk,
            selectedIndex,
            selectedType,
            applyCurrentQuery,
        } = this.state;
        const res = await this.fetchData();
        const file = new File(
            [JSON.stringify(res, null, 4)],
            `data_${selectedIndex}${
                applyCurrentQuery ? '' : `_${selectedType}`
            }_${selectedChunk.replace(/-/g, '_')}.json`,
            {
                type: 'application/json;charset=utf-8',
            },
        );
        saveAs(file);
    };

    handlePageSelect = value => {
        this.setState({
            selectedChunk: value,
        });
    };

    resetSelectedChunk = () => {
        const { countChunks } = this.state;
        const chunkList = Object.keys(countChunks);
        this.setState({
            selectedChunk: chunkList[0],
            searchAfterData: '',
        });
    };

    handleIndexChange = selectedIndex => {
        this.setState(
            {
                selectedIndex,
                types: this.props.indexTypeMap[selectedIndex],
                selectedType: this.props.indexTypeMap[selectedIndex][0],
            },
            () => {
                this.fetchCount();
            },
        );
    };

    handleTypeChange = selectedType => {
        this.setState(
            {
                selectedType,
            },
            () => {
                this.fetchCount();
            },
        );
    };

    handleApplyQueryChange = e => {
        const {
            target: { checked },
        } = e;

        const { totalCount } = this.state;
        const { stats } = this.props;
        const chunks = checked
            ? getChunks(stats.totalResults)
            : getChunks(totalCount);
        this.setState({
            applyCurrentQuery: checked,
            countChunks: chunks,
            selectedChunk: Object.keys(chunks)[0],
        });
    };

    render() {
        const {
            isShowingModal,
            isDownloading,
            error,
            isFetchingCount,
            countChunks,
            selectedChunk,
            searchAfterData,
            selectedIndex,
            selectedType,
            types,
            applyCurrentQuery,
        } = this.state;

        const { indexTypeMap, stats, version } = this.props;
        const chunkList = Object.keys(countChunks);
        return (
            <Fragment>
                <Button
                    icon={<DownloadOutlined />}
                    css={{ marginRight: '5px' }}
                    onClick={this.toggleModal}
                >
                    Export
                </Button>

                <Modal
                    open={isShowingModal}
                    afterClose={this.handleAfterClose}
                    onCancel={this.toggleModal}
                    destroyOnClose
                    maskClosable={false}
                    title="Export Data"
                    footer={[
                        <Button
                            key="csv"
                            type="primary"
                            onClick={this.onCSVClick}
                            disabled={isDownloading || isFetchingCount || error}
                        >
                            Download as CSV
                        </Button>,
                        <Button
                            key="json"
                            type="primary"
                            onClick={this.onJSONClick}
                            disabled={isDownloading || isFetchingCount || error}
                        >
                            Download as JSON
                        </Button>,
                    ]}
                >
                    <Row>
                        <Col span={12}>
                            <Item style={{ marginRight: '15px' }} label="Index">
                                <Select
                                    defaultValue={selectedIndex}
                                    onChange={this.handleIndexChange}
                                    css={{
                                        width: '95%',
                                    }}
                                >
                                    {Object.keys(indexTypeMap).map(index => (
                                        <Option key={index} value={index}>
                                            {index}
                                        </Option>
                                    ))}
                                </Select>
                            </Item>
                        </Col>
                        {(version === 5 || version === 6) && (
                            <Col span={12}>
                                <Item
                                    style={{ marginRight: '15px' }}
                                    label="Document Type"
                                >
                                    <Select
                                        value={selectedType}
                                        onChange={this.handleTypeChange}
                                        css={{
                                            width: '100%',
                                        }}
                                    >
                                        {types.map(type => (
                                            <Option key={type} value={type}>
                                                {type}
                                            </Option>
                                        ))}
                                    </Select>
                                </Item>
                            </Col>
                        )}
                    </Row>
                    <Alert
                        type="info"
                        description={
                            <React.Fragment>
                                <div>
                                    Data is exported in set of up to{' '}
                                    <b>{MAX_DATA}</b> documents and in a
                                    sequential manner.
                                </div>
                            </React.Fragment>
                        }
                    />
                    <br />
                    {!isFetchingCount && selectedChunk && (
                        <p>
                            Now you can export <b>{selectedChunk}</b> documents
                            in CSV or JSON format by selecting appropriate
                            option.
                        </p>
                    )}

                    {!isFetchingCount &&
                        searchAfterData &&
                        selectedChunk !== chunkList[0] && (
                            <p>
                                To export from beginning,{' '}
                                <span
                                    onClick={this.resetSelectedChunk}
                                    css={{
                                        color: colors.primary,
                                        cursor: 'pointer',
                                    }}
                                >
                                    click here to reset
                                </span>
                                .
                            </p>
                        )}
                    <Flex
                        flexDirection="column"
                        alignItems="center"
                        justifyContent="center"
                    >
                        {isFetchingCount && (
                            <Flex
                                flexDirection="column"
                                alignItems="center"
                                justifyContent="center"
                            >
                                <Spin css={{ marginBottom: '20px' }} />
                            </Flex>
                        )}
                        <br />

                        {isDownloading && (
                            <Flex
                                flexDirection="column"
                                alignItems="center"
                                justifyContent="center"
                            >
                                <Spin css={{ marginBottom: '20px' }} />
                                We are fetching the result data, please wait...
                            </Flex>
                        )}
                        <Checkbox
                            checked={applyCurrentQuery}
                            onChange={this.handleApplyQueryChange}
                            css={{
                                width: '100%',
                            }}
                        >
                            Export {numberWithCommas(stats.totalResults || 0)}{' '}
                            documents (current view)
                        </Checkbox>
                    </Flex>
                </Modal>
            </Fragment>
        );
    }
}

const mapStateToProps = state => ({
    url: getUrl(state),
    version: getVersion(state),
    indexTypeMap: getIndexTypeMap(state),
    stats: getStats(state),
    query: getQuery(state),
    visibleColumns: getVisibleColumns(state),
});

const mapDispatchToProps = {
    addDataRequest,
};

export default connect(mapStateToProps, mapDispatchToProps)(ExportData);