Source

Body/Project/Tabs/Classification.js

import React, { Component } from "react";
import PropTypes from "prop-types";
import { nonNullProperty } from "../../../Utils/utilFunctions";
import { downloadMatrix, fetchClassification } from "../../../Utils/utilFunctions/fetchFunctions";
import { parseFormData } from "../../../Utils/utilFunctions/fetchFunctions/parseFormData";
import { getItemName, parseClassifiedItems } from "../../../Utils/utilFunctions/parseItems";
import { parseClassifiedListItems } from "../../../Utils/utilFunctions/parseListItems";
import { parseClassificationParams } from "../../../Utils/utilFunctions/parseParams";
import TabBody from "../../../Utils/Containers/TabBody";
import filterFunction from "../Filtering/FilterFunction";
import FilterTextField from "../Filtering/FilterTextField";
import DefaultClassificationResultSelector from "../Calculations/DefaultClassificationResultSelector";
import TypeOfClassifierSelector from "../Calculations/TypeOfClassifierSelector";
import { CalculateButton, MatrixButton, MatrixDownloadButton, SettingsButton } from "../../../Utils/Buttons";
import CustomBox from "../../../Utils/Containers/CustomBox";
import CustomDrawer from "../../../Utils/Containers/CustomDrawer";
import MatrixDialog from "../../../Utils/Dialogs/MatrixDialog";
import StyledDivider from "../../../Utils/DataDisplay/StyledDivider";
import CircleHelper from "../../../Utils/Feedback/CircleHelper";
import CSVDialog from "../../../Utils/Dialogs/CSVDialog";
import { ClassifiedObjectDialog } from "../../../Utils/Dialogs/DetailsDialog"
import StyledAlert from "../../../Utils/Feedback/StyledAlert";
import CustomButtonGroup from "../../../Utils/Inputs/CustomButtonGroup";
import CustomUpload from "../../../Utils/Inputs/CustomUpload";
import CustomHeader from "../../../Utils/Surfaces/CustomHeader";
import {AttributesMenu} from "../../../Utils/Menus/AttributesMenu";

/**
 * <h3>Overview</h3>
 * The classification tab in RuLeStudio.
 * Presents the list of all object from information table with suggested classification based on generated rules.
 *
 * @constructor
 * @category Project
 * @subcategory Tabs
 * @param {Object} props
 * @param {string} props.objectGlobalName - The global visible object name used by all tabs as reference.
 * @param {function} props.onDateUploaded - Callback fired when tab receives information that new data was uploaded.
 * @param {function} props.onTabChange - Callback fired when a tab is changed and there are unsaved changes in this tab.
 * @param {Object} props.project - Current project.
 * @param {string} props.serverBase - The host and port in the URL of an API call.
 * @param {function} props.showAlert - Callback fired when results in this tab are based on outdated information table.
 * @param {number} props.value - The index of a selected tab.
 * @returns {React.Component}
 */
class Classification extends Component {
    constructor(props) {
        super(props);

        this.state = {
            loading: false,
            data: null,
            items: null,
            displayedItems: [],
            externalData: false,
            parameters: {
                defaultClassificationResultType: "majorityDecisionClass",
                classifierType: "SimpleRuleClassifier",
            },
            parametersSaved: true,
            refreshNeeded: {
                attributesMenu: false,
                matrix: false
            },
            selected: {
                item: null,
                action: 0
            },
            open: {
                details: false,
                matrix: false,
                settings: false,
                csv: false
            },
            attributesMenuEl: null,
            alertProps: undefined,
        };

        this.upperBar = React.createRef();
    }

    /**
     * <h3>Overview</h3>
     * Makes an API call on classification to receive current copy of classification from server.
     * Then, updates state and makes necessary changes in display.
     *
     * @function
     * @memberOf Classification
     */
    getClassification = () => {
        const { project, serverBase } = this.props;
        const pathParams = { projectId: project.id };
        const method = "GET";

        fetchClassification(
            pathParams, method, null, serverBase
        ).then(result => {
            if (this._isMounted && result != null && result.hasOwnProperty("Objects")
                && result.hasOwnProperty("objectNames")) {

                const items = parseClassifiedItems(result.Objects, result.objectNames);
                const resultParameters = result.hasOwnProperty("parameters") ?
                    parseClassificationParams(result.parameters) : { };

                this.setState(({parameters}) => ({
                    data: result,
                    items: items,
                    displayedItems: items,
                    parameters: { ...parameters, ...resultParameters }
                }));

                if (result.hasOwnProperty("isCurrentData")) {
                    const messages = result.hasOwnProperty("errorMessages") ?
                        result.errorMessages : null;
                    this.props.showAlert(this.props.value, !result.isCurrentData, messages);
                }

                if (result.hasOwnProperty("externalData")) {
                    this.props.onDataUploaded(result.externalData);
                }
            }
        }).catch(error => {
            this.onSnackbarOpen(error, () => {
                if (this._isMounted) {
                    this.setState({
                        data: null,
                        items: null,
                        displayedItems: []
                    });
                }
            });
        }).finally(() => {
            if (this._isMounted) {
                const { project: { parameters, parametersSaved, classifyAction }} = this.props;
                const { defaultClassificationResultType, classifierType } = parameters;

                this.setState(({parameters, selected}) => ({
                    loading: false,
                    parameters: parametersSaved ?
                        parameters : { ...parameters, ...{ defaultClassificationResultType, classifierType }},
                    parametersSaved: parametersSaved,
                    selected: { ...selected, item: null, action: classifyAction }
                }));
            }
        });
    }

    /**
     * <h3>Overview</h3>
     * A component's lifecycle method. Fired once when component was mounted.
     *
     * <h3>Goal</h3>
     * Method calls {@link getClassification}.
     *
     * @function
     * @memberOf Classification
     */
    componentDidMount() {
        this._isMounted = true;

        this.setState({ loading: true }, this.getClassification);
    }

    /**
     * <h3>Overview</h3>
     * A component's lifecycle method. Fired after a component was updated.
     *
     * <h3>Goal</h3>
     * If project was changed, method saves changes from previous project
     * and calls {@link getClassification} to receive the latest copy of classification.
     *
     * @function
     * @memberOf Classification
     * @param {Object} prevProps - Old props that were already replaced.
     * @param {Object} prevState - Old state that was already replaced.
     * @param {Object} snapshot - Returned from another lifecycle method <code>getSnapshotBeforeUpdate</code>. Usually undefined.
     */
    componentDidUpdate(prevProps, prevState, snapshot) {
        if (prevProps.project.id !== this.props.project.id) {
            const { parametersSaved, selected: { action } } = prevState;
            let project = { ...prevProps.project };

            if (!parametersSaved) {
                const { parameters } = prevState;

                project.parameters = { ...project.parameters, ...parameters};
                project.parametersSaved = parametersSaved;
            }

            project.classifyAction = action;
            this.props.onTabChange(project);
            this.setState({ loading: true }, this.getClassification);
        }
    }

    /**
     * <h3>Overview</h3>
     * A component's lifecycle method. Fired when component was requested to be unmounted.
     *
     * <h3>Goal</h3>
     * Method saves changes from current project.
     *
     * @function
     * @memberOf Classification
     */
    componentWillUnmount() {
        this._isMounted = false;
        const { parametersSaved, selected: { action } } = this.state;
        let project = JSON.parse(JSON.stringify(this.props.project));

        if (!parametersSaved) {
            const { parameters } = this.state;

            project.parameters = { ...project.parameters, ...parameters };
            project.parametersSaved = parametersSaved;
        }

        project.classifyAction = action;
        this.props.onTabChange(project);
    }

    /**
     * <h3>Overview</h3>
     * Makes an API call on classification to classify objects from current project or uploaded objects
     * with selected parameters.
     * Then, updates state and makes necessary changes in display.
     *
     * @function
     * @memberOf Classification
     * @param {string} method - A HTTP method such as GET, POST or PUT.
     * @param {Object} data - The body of the message.
     */
    calculateClassification = (method, data) => {
        const { project, serverBase } = this.props;

        this.setState({
            loading: true,
        }, () => {
            const pathParams = { projectId: project.id };

            fetchClassification(
                pathParams, method, data, serverBase
            ).then(result => {
                if (result != null) {
                    if (this._isMounted && result.hasOwnProperty("Objects")
                        && result.hasOwnProperty("objectNames")) {

                        const items = parseClassifiedItems(result.Objects, result.objectNames);

                        this.setState({
                            data: result,
                            items: items,
                            displayedItems: items,
                            parametersSaved: true,
                            refreshNeeded: {
                                attributesMenu: true,
                                matrix: true
                            }
                        });
                    }

                    let projectCopy = JSON.parse(JSON.stringify(project));
                    const resultParameters = result.hasOwnProperty("parameters") ?
                        parseClassificationParams(result.parameters) : { };

                    projectCopy.parameters = { ...project.parameters, ...resultParameters }
                    projectCopy.parametersSaved = true;
                    this.props.onTabChange(projectCopy);

                    if (result.hasOwnProperty("isCurrentData")) {
                        const messages = result.hasOwnProperty("errorMessages")
                            ? result.errorMessages : null;
                        this.props.showAlert(this.props.value, !result.isCurrentData, messages);
                    }

                    if (result.hasOwnProperty("externalData")) {
                        this.props.onDataUploaded(result.externalData);
                    }
                }
            }).catch(exception => {
                this.onSnackbarOpen(exception, () => {
                    if (this._isMounted) {
                        this.setState({
                            data: null,
                            items: null,
                            displayedItems: []
                        });
                    }
                });
            }).finally(() => {
                if (this._isMounted) {
                    this.setState(({selected}) => ({
                        loading: false,
                        selected: { ...selected, item: null }
                    }));
                }
            });
        });
    }

    /**
     * <h3>Overview</h3>
     * Calls {@link calculateClassification} to classify objects from current project.
     *
     * @function
     * @memberOf Classification
     */
    onClassifyData = () => {
        const { parameters } = this.state;

        const method = "PUT";
        const data = parseFormData(parameters, null);

        this.calculateClassification(method, data);
    };

    /**
     * <h3>Overview</h3>
     * Calls {@link calculateClassification} to classify objects from uploaded file.
     * If uploaded file is in CSV format, method opens {@link CSVDialog} to specify CSV attributes.
     *
     * @function
     * @memberOf Classification
     * @param {Object} event - Represents an event that takes place in DOM.
     */
    onUploadData = (event) => {
        event.persist();

        if (event.target.files) {
            if (event.target.files[0].type !== "application/json") {
                this.csvFile = event.target.files[0];

                this.setState(({open}) => ({
                    open: { ...open, csv: true }
                }));
            } else {
                const { parameters } = this.state;

                let method = "PUT";
                let files = { externalDataFile: event.target.files[0] };

                let data = parseFormData(parameters, files);
                this.calculateClassification(method, data);
            }
        }
    };

    /**
     * <h3>Overview</h3>
     * Callback fired when {@link CSVDialog} requests to be closed.
     * If the user specified CSV attributes, method calls {@link calculateClassification} to
     * classify objects from uploaded file.
     *
     * @function
     * @memberOf Classification
     * @param {Object} csvSpecs - An object representing CSV attributes.
     */
    onCSVDialogClose = (csvSpecs) => {
        this.setState(({open}) => ({
            open: { ...open, csv: false }
        }), () => {
            if (csvSpecs && Object.keys(csvSpecs).length) {
                const { parameters } = this.state;

                let method = "PUT";
                let files = { externalDataFile: this.csvFile };

                let data = parseFormData({ ...parameters, ...csvSpecs }, files);
                this.calculateClassification(method, data);
            }
        });
    }

    /**
     * <h3>Overview</h3>
     * Callback fired when the user requests to download misclassification matrix.
     * Method makes an API call to download the resource.
     *
     * @function
     * @memberOf Classification
     */
    onSaveToFile = () => {
        const { project, serverBase } = this.props;
        const pathParams = { projectId: project.id };
        const queryParams = { typeOfMatrix: "classification" };

        downloadMatrix(pathParams, queryParams, serverBase)
            .catch(this.onSnackbarOpen);
    };

    onDefaultClassificationResultChange = (event) => {
        const { loading } = this.state;

        if (!loading) {
            this.setState(({parameters}) => ({
                parameters: {...parameters, defaultClassificationResultType: event.target.value},
                parametersSaved: false
            }));
        }
    };

    onClassifierTypeChange = (event) => {
        const { loading } = this.state;

        if (!loading) {
            this.setState(({parameters}) => ({
                parameters: {...parameters, classifierType: event.target.value},
                parametersSaved: false
            }));
        }
    };

    onClassifyActionChange = (index) => {
        this.setState(({selected}) => ({
            selected: { ...selected, action: index }
        }));
    };

    /**
     * <h3>Overview</h3>
     * Filters items from {@link Classification}'s state.
     * Method uses {@link filterFunction} to filter items.
     *
     * @function
     * @memberOf Classification
     * @param {Object} event - Represents an event that takes place in DOM.
     */
    onFilterChange = (event) => {
        const { loading, items } = this.state;

        if (!loading && Array.isArray(items) && items.length) {
            const filteredItems = filterFunction(event.target.value.toString(), items.slice());

            this.setState(({selected}) => ({
                displayedItems: filteredItems,
                selected: { ...selected, item: null }
            }));
        }
    };

    toggleOpen = (name) => {
        this.setState(({open}) => ({
            open: {...open, [name]: !open[name]}
        }));
    };

    onDetailsOpen = (index) => {
        const { items } = this.state;

        this.setState(({open, selected}) => ({
            open: { ...open, details: true, settings: false },
            selected: { ...selected, item: items[index] }
        }));
    };

    onObjectNamesChange = (names) => {
        this.setState(({items, displayedItems}) => ({
            items: items.map((item, index) => {
                item.name = getItemName(index, names);
                return item;
            }),
            displayedItems: displayedItems.map((item, index) => {
                item.name = getItemName(index, names);
                return item;
            })
        }));
    };

    onAttributesMenuOpen = (event) => {
        const currentTarget = event.currentTarget;

        this.setState({
            attributesMenuEl: currentTarget
        });
    };

    onAttributesMenuClose = () => {
        this.setState({
            attributesMenuEl: null
        });
    };

    onComponentRefresh = (target) => {
        this.setState(({refreshNeeded}) => ({
            refreshNeeded: { ...refreshNeeded, [target]: false }
        }));
    };

    onSnackbarOpen = (exception, setStateCallback) => {
        if (!(exception.hasOwnProperty("type") && exception.type === "AlertError")) {
            console.error(exception);
            return;
        }

        if (this._isMounted) {
            this.setState({ alertProps: exception }, setStateCallback);
        }
    };

    onSnackbarClose = (event, reason) => {
        if (reason !== 'clickaway') {
            this.setState(({alertProps}) => ({
                alertProps: {...alertProps, open: false}
            }));
        }
    };

    render() {
        const {
            loading,
            data,
            items,
            displayedItems,
            parameters,
            refreshNeeded,
            selected,
            open,
            attributesMenuEl,
            alertProps
        } = this.state;

        const { objectGlobalName, project: { id: projectId }, serverBase } = this.props;

        return (
            <CustomBox id={"classification"} variant={"Tab"}>
                <CustomDrawer
                    id={"classification-settings"}
                    open={open.settings}
                    onClose={() => this.toggleOpen("settings")}
                    placeholder={this.upperBar.current ? this.upperBar.current.offsetHeight : undefined}
                >
                    <TypeOfClassifierSelector
                        TextFieldProps={{
                            onChange: this.onClassifierTypeChange,
                            value: parameters.classifierType
                        }}
                    />
                    <DefaultClassificationResultSelector
                        TextFieldProps={{
                            onChange: this.onDefaultClassificationResultChange,
                            value: parameters.defaultClassificationResultType
                        }}
                    />
                </CustomDrawer>
                <CustomBox customScrollbar={true} id={"classification-content"} variant={"TabBody"}>
                    <CustomHeader id={"classification-header"} paperRef={this.upperBar}>
                        <SettingsButton onClick={() => this.toggleOpen("settings")} />
                        <StyledDivider margin={16} />
                        <CustomButtonGroup
                            onActionSelected={this.onClassifyActionChange}
                            options={["Classify current data", "Choose new data & classify"]}
                            selected={selected.action}
                            tooltips={"Click on settings button on the left to customize parameters"}
                            WrapperProps={{
                                id: "classification-split-button"
                            }}
                        >
                            <CalculateButton
                                aria-label={"classify-current-file"}
                                disabled={loading}
                                onClick={this.onClassifyData}
                            >
                                Classify current data
                            </CalculateButton>
                            <CustomUpload
                                accept={".json,.csv"}
                                id={"classify-new-file"}
                                onChange={this.onUploadData}
                            >
                                <CalculateButton
                                    aria-label={"classify-new-file"}
                                    disabled={loading}
                                    component={"span"}
                                >
                                    Choose new data & classify
                                </CalculateButton>
                            </CustomUpload>
                        </CustomButtonGroup>
                        <CircleHelper
                            size={"smaller"}
                            title={"Attributes are taken from DATA."}
                            TooltipProps={{ placement: "bottom"}}
                            WrapperProps={{ style: { marginLeft: 16 }}}
                        />
                        {data &&
                            <React.Fragment>
                                <StyledDivider margin={16} />
                                <MatrixButton
                                    onClick={() => this.toggleOpen("matrix")}
                                    tooltip={"Show ordinal misclassification matrix and it's details"}
                                />
                            </React.Fragment>
                        }
                        <span style={{flexGrow: 1}} />
                        <FilterTextField onChange={this.onFilterChange} />
                    </CustomHeader>
                    <TabBody
                        content={parseClassifiedListItems(displayedItems)}
                        id={"classification-list"}
                        isArray={Array.isArray(displayedItems) && Boolean(displayedItems.length)}
                        isLoading={loading}
                        ListProps={{
                            onItemSelected: this.onDetailsOpen
                        }}
                        ListSubheaderProps={{
                            onSettingsClick: this.onAttributesMenuOpen,
                            style: this.upperBar.current ? { top: this.upperBar.current.offsetHeight } : undefined
                        }}
                        noFilterResults={!displayedItems}
                        subheaderContent={[
                            {
                                label: "Number of objects:",
                                value: Array.isArray(displayedItems) ? displayedItems.length : "-"
                            },
                            {
                                label: "Calculated in:",
                                value: nonNullProperty(data, "calculationsTime") ?
                                    data.calculationsTime : "-"
                            }
                        ]}
                    />
                    {selected.item != null &&
                        <ClassifiedObjectDialog
                            disableAttributesMenu={false}
                            item={selected.item}
                            objectGlobalName={objectGlobalName}
                            onClose={() => this.toggleOpen("details")}
                            onSnackbarOpen={this.onSnackbarOpen}
                            open={open.details}
                            projectId={projectId}
                            resource={"classification"}
                            serverBase={serverBase}
                        />
                    }
                    {Array.isArray(items) && items.length > 0 &&
                        <MatrixDialog
                            onClose={() => this.toggleOpen("matrix")}
                            onMatrixRefresh={() => this.onComponentRefresh("matrix")}
                            onSnackbarOpen={this.onSnackbarOpen}
                            open={open.matrix}
                            projectId={projectId}
                            refreshNeeded={refreshNeeded.matrix}
                            resource={"classification"}
                            saveMatrix={this.onSaveToFile}
                            serverBase={serverBase}
                            title={
                                <React.Fragment>
                                    <MatrixDownloadButton
                                        onClick={this.onSaveToFile}
                                        tooltip={"Download matrix (txt)"}
                                    />
                                    <span aria-label={"matrix title"} style={{paddingLeft: 8}}>
                                        Ordinal misclassification matrix and details
                                    </span>
                                </React.Fragment>
                            }
                        />
                    }
                    <AttributesMenu
                        ListProps={{
                            id: "classification-main-desc-attributes-menu"
                        }}
                        MuiMenuProps={{
                            anchorEl: attributesMenuEl,
                            onClose: this.onAttributesMenuClose
                        }}
                        objectGlobalName={objectGlobalName}
                        onAttributesRefreshed={() => this.onComponentRefresh("attributesMenu")}
                        onObjectNamesChange={this.onObjectNamesChange}
                        onSnackbarOpen={this.onSnackbarOpen}
                        projectId={projectId}
                        refreshNeeded={refreshNeeded.attributesMenu}
                        resource={"classification"}
                        serverBase={serverBase}
                    />
                    <CSVDialog onConfirm={this.onCSVDialogClose} open={open.csv} />
                </CustomBox>
                <StyledAlert {...alertProps} onClose={this.onSnackbarClose} />
            </CustomBox>
        )
    }
}

Classification.propTypes = {
    objectGlobalName: PropTypes.string,
    onDataUploaded: PropTypes.func,
    onTabChange: PropTypes.func,
    project: PropTypes.object,
    serverBase: PropTypes.string,
    showAlert: PropTypes.func,
    value: PropTypes.number
};

export default Classification;