Source

Body/Project/ProjectTabs.js

import React from 'react';
import PropTypes from 'prop-types';
import Classification from "./Tabs/Classification";
import CrossValidation from "./Tabs/CrossValidation";
import Cones from "./Tabs/Cones";
import Data from "./Data/DisplayData";
import Rules from "./Tabs/Rules";
import Unions from "./Tabs/Unions";
import { fetchData, fetchProject } from "../../Utils/utilFunctions/fetchFunctions";
import StyledLinkTab from "../../Utils/Navigation/StyledLinkTab";
import StyledTabs from "../../Utils/Navigation/StyledTabs";
import ExternalFile from "../../Utils/Feedback/CustomIcons/ExternalFile";
import OutdatedData from "../../Utils/Feedback/AlertBadge/Alerts/OutdatedData";
import { Route, Switch } from 'react-router-dom';
import { tabNames } from "../../Utils/Constants/TabsNamesInPath";

/**
 * <h3>Overview</h3>
 * The Project section in RuLeStudio. Allows a user to choose between tabs.
 * If necessary, displays information about outdated results shown in currently selected tab.
 *
 * @constructor
 * @category Project
 * @param {Object} props
 * @param {boolean} props.deleting - If <code>true</code> the project was requested to be deleted.
 * @param {string} props.objectGlobalName - The global visible object name used by all tabs as reference.
 * @param {function} props.onSnackbarOpen - Callback fired when the component request to display an error.
 * @param {Object} props.project - Current project.
 * @param {string} props.serverBase - The host and port in the URL of an API call.
 * @param {function} props.updateProject - Callback fired when a part of current project was changed.
 * @returns {React.PureComponent}
 */
class ProjectTabs extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            informationTable: null,
            isUpdateNecessary: false,
            refreshNeeded: false,
            loading: false,
            selected: 0,
            showAlert: Array(5).fill(false),
            showExternalRules: false,
            showExternalData: false,
            alertMessages: Array(5).fill(null)
        };
    }

    /**
     * <h3>Overview</h3>
     * Updates alerts based on the response from server.
     *
     * @function
     * @memberOf ProjectTabs
     * @param {Object} result - The response from the server.
     */
    updateAlerts = (result) => {
        /* Update alert in Dominance cones */
        if (result.dominanceCones != null && result.dominanceCones.hasOwnProperty("isCurrentData")) {
            this.setState(({showAlert, alertMessages}) => {
                showAlert[0] = !result.dominanceCones.isCurrentData;
                alertMessages[0] = result.dominanceCones.hasOwnProperty("errorMessages") ?
                    result.dominanceCones.errorMessages : null;
                return { showAlert, alertMessages };
            });
        } else {
            /* Reset alert if there are no dominance cones */
            this.setState(({showAlert, alertMessages}) => {
                showAlert[0] = false;
                alertMessages[0] = null;
                return { showAlert, alertMessages };
            });
        }

        /* Update alert in Class unions */
        if (result.unions != null && result.unions.hasOwnProperty("isCurrentData")) {
            this.setState(({showAlert, alertMessages}) => {
                showAlert[1] = !result.unions.isCurrentData;
                alertMessages[1] = result.unions.hasOwnProperty("errorMessages") ?
                    result.unions.errorMessages : null
                return { showAlert, alertMessages };
            });
        } else {
            /* Reset alert if there are no class unions */
            this.setState(({showAlert, alertMessages}) => {
                showAlert[1] = false;
                alertMessages[1] = null;
                return { showAlert, alertMessages };
            });
        }

        /* Update alerts in Rules */
        if (result.rules != null) {
            if (result.rules.hasOwnProperty("isCurrentData")) {
                this.setState(({showAlert, alertMessages}) => {
                    showAlert[2] = !result.rules.isCurrentData;
                    alertMessages[2] = result.rules.hasOwnProperty("errorMessages") ?
                        result.rules.errorMessages : null;
                    return { showAlert, alertMessages };
                });
            }

            if (result.rules.hasOwnProperty("externalRules")) {
                this.setState({
                    showExternalRules: result.rules.externalRules
                });
            }
        } else {
            /* Reset alerts if there are no rules*/
            this.setState(({showAlert, alertMessages}) => {
                showAlert[2] = false;
                alertMessages[2] = null;
                return { showAlert, showExternalRules: false, alertMessages };
            });
        }

        /* Update alerts in Classification */
        if (result.classification != null) {
            if (result.classification.hasOwnProperty("isCurrentData")) {
               this.setState(({showAlert, alertMessages}) => {
                   showAlert[3] = !result.classification.isCurrentData;
                   alertMessages[3] = result.classification.hasOwnProperty("errorMessages") ?
                       result.classification.errorMessages : null;
                   return { showAlert, alertMessages }
               });
            }

            if (result.classification.hasOwnProperty("externalData")) {
                this.setState({
                    showExternalData: result.classification.externalData
                });
            }
        } else {
            /* Reset alerts if there are no classification results */
            this.setState(({showAlert, alertMessages}) => {
                showAlert[3] = false;
                alertMessages[3] = null;
                return { showAlert, showExternalData: false, alertMessages };
            });
        }

        /* Update alerts in CrossValidation */
        if (result.crossValidation != null && result.crossValidation.hasOwnProperty("isCurrentData")) {
            this.setState(({showAlert, alertMessages}) => {
                showAlert[4] = !result.crossValidation.isCurrentData;
                alertMessages[4] = result.crossValidation.hasOwnProperty("errorMessages") ?
                    result.crossValidation.errorMessages : null;
                return { showAlert, alertMessages };
            });
        } else {
            /* Reset alert if there are no cross-validation results */
            this.setState(({showAlert, alertMessages}) => {
                showAlert[4] = false;
                alertMessages[4] = null;
                return { showAlert, alertMessages };
            });
        }
    };

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

            const pathParams = { projectId };
            const method = "GET"

            fetchProject(
                pathParams, method, null, serverBase
            ).then(result => {
                if (this._isMounted && result != null) {
                    this.updateAlerts(result);
                }
            }).catch(
                this.props.onSnackbarOpen
            ).finally(() => {
                if (this._isMounted) {
                    this.setState({
                        loading: false
                    });
                }
            });
        });
    }

    /**
     * <h3>Overview</h3>
     * Utilizes {@link fetchData} to perform an API call with POST method and information table in body.
     *
     * <h3>Goal</h3>
     * The goal of this function is to save user's changes made in information table.
     *
     * @function
     * @memberOf ProjectTabs
     * @param {string} projectId - The identifier of a selected project.
     * @param {Object} informationTable - The local copy of an information table that will be sent to server.
     * @param {function} [finallyCallback] - The callback fired in finally part of the fetch function.
     */
    updateProjectOnServer = (projectId, informationTable, finallyCallback) => {
        const pathParams = { projectId };
        const method = "POST";
        const body = new FormData();
        body.append("metadata", JSON.stringify(informationTable.attributes));
        body.append("data", JSON.stringify(informationTable.objects));
        const { serverBase } = this.props;

        fetchData(
            pathParams, method, body, serverBase
        ).then(result => {
            if (this._isMounted && result != null) {
                this.updateAlerts(result);
            }
        }).catch(
            this.props.onSnackbarOpen
        ).finally(
            finallyCallback
        );
    };

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

    shouldComponentUpdate(nextProps, nextState, nextContext) {
        return this.props !== nextProps || this.state !== nextState;
    }

    /**
     * <h3>Overview</h3>
     * A component's lifecycle method. Fired after a component was updated.
     *
     * <h3>Goal</h3>
     * Checks if project was changed. If a new project was forwarded, method makes an API call to retrieve that project
     * and saves changes from old project if necessary.
     *
     * @function
     * @memberOf ProjectTabs
     * @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 { project: { id: projectId }} = prevProps;
            const { informationTable, isUpdateNecessary, selected } = this.state;
          
            if (isUpdateNecessary && selected === 0) {
                this.setState({
                    loading: true
                }, () => {
                    this.updateProjectOnServer(projectId, informationTable, () => {
                        if (this._isMounted) {
                            this.setState({
                                isUpdateNecessary: false,
                                loading: false
                            });
                        }
                    });
                });
            } else if (selected !== 0) {
                this.setState({
                    refreshNeeded: true
                });
            }

            this.getProject();
            this.activateTabUpToURL();
        }

        if (this.props.history.action === "POP") {
            if (prevProps.location !== this.props.location) {
                this.activateTabUpToURL();
            }
        }
    };

    /**
     * <h3>Overview</h3>
     * A component's lifecycle method. Fired when component was requested to be unmounted.
     *
     * <h3>Goal</h3>
     * If there were any unsaved changes, method calls {@link updateProjectOnServer}.
     *
     * @function
     * @memberOf ProjectTabs
     */
    componentWillUnmount() {
        this._isMounted = false;

        const { deleting, project: { id: projectId }} = this.props;
        const { informationTable, isUpdateNecessary, selected } = this.state;

        if (isUpdateNecessary && selected === 0 && !deleting) {
            this.updateProjectOnServer(projectId, informationTable, () => {
                if (this._isMounted) {
                    this.setState({
                        isUpdateNecessary: false
                    });
                }
            });
        }
    };

    /**
     * <h3>Overview</h3>
     * Fired when a tab is changed. If user had unsaved changes in {@link Data} tab,
     * method calls {@link updateProjectOnServer} to save them on server.
     *
     * @function
     * @memberOf ProjectTabs
     * @param {Object} event - Represents an event that takes place in DOM.
     * @param {number} newValue - The id of tab that was selected.
     */
    onTabChange = (event, newValue) => {
        const { project: { id: projectId }} = this.props;
        const { informationTable, isUpdateNecessary, selected } = this.state;

        this.setState({
            selected: newValue
        }, () => {
            if (isUpdateNecessary && selected === 0 && newValue !== 0) {
                this.setState({
                    loading: true
                }, () => {
                    this.updateProjectOnServer(projectId, informationTable, () => {
                        if (this._isMounted) {
                            this.setState({
                                isUpdateNecessary: false,
                                loading: false
                            });
                        }
                    });
                });
            }
        });
    };

    /**
     * <h3>Overview</h3>
     * Forwarded to the {@link Data} tab. Fired when a user makes changes in the information table.
     * Saves modified project in the component's state.
     *
     * @function
     * @memberOf ProjectTabs
     * @param {Object} informationTable - Modified information table from the {@link Data} tab.
     * @param {boolean} isUpdateNecessary - If <code>true</code> information table will be sent to server on exit.
     */
    onDataChange = (informationTable, isUpdateNecessary) => {
        this.setState({
            informationTable: informationTable,
            isUpdateNecessary: isUpdateNecessary,
            refreshNeeded: false
        });
    };

    /**
     * <h3>Overview</h3>
     * Forwarded to all tabs. Fired when a tab receives information from the server that current results are outdated.
     *
     * @function
     * @memberOf ProjectTabs
     * @param {number} index - The index of a selected tab.
     * @param {boolean} show - If <code>true</code> an alert about outdated results in tab will be displayed.
     * @param {string[]} [messages] - Optional messages displayed in alert.
     */
    showAlert = (index, show, messages) => {
        this.setState(({showAlert, alertMessages}) => {
            showAlert[index] = show;
            alertMessages[index] = messages;

            return {
                showAlert: showAlert,
                alertMessages: alertMessages
            };
        });
    };

    /**
     * <h3>Overview</h3>
     * Forwarded to the {@link Rules} tab. Fired when a user uploads rule set.
     * Saves this information in the component's state.
     *
     * @function
     * @memberOf ProjectTabs
     * @param {boolean} isExternal - If <code>true</code> alert will be displayed that rule set was uploaded.
     */
    onRulesUploaded = (isExternal) => {
        this.setState({
            showExternalRules: isExternal
        });
    };

    /**
     * <h3>Overview</h3>
     * Forwarded to the {@link Rules} and {@link Classification} tabs. Fired when a user uploads information table.
     * Saves this information in the component's state.
     *
     * @function
     * @memberOf ProjectTabs
     * @param {boolean} isExternal - If <code>true</code> alert will be displayed that external data was classified.
     */
    onDataUploaded = (isExternal) => {
        this.setState({
            showExternalData: isExternal
        });
    };

    getTabProps = (index) => ({
        id: `project-tab-${index}`,
        'aria-controls': `project-tabpanel-${index}`
    });

    getTabBodyProps = (index) => ({
        objectGlobalName: this.props.objectGlobalName,
        onTabChange: this.props.updateProject,
        project: this.props.project,
        serverBase: this.props.serverBase,
        showAlert: this.showAlert,
        value: index
    });

    activateTabUpToURL = () => {
        const url = window.location.href.toString();
        const urlSplitted = url.split('/');

        if (urlSplitted.length < 5) {
            this.props.history.replace({
                pathname: `/${this.props.project.id}/${tabNames[0]}`,
                state: { projectId: this.props.project.id }
            });
            this.onTabChange(null,0);
        }
        else {
            switch (urlSplitted[4]) {
                case tabNames[1]:
                    this.onTabChange(null,1);
                    break;
                case tabNames[2]:
                    this.onTabChange(null,2);
                    break;
                case tabNames[3]:
                    this.onTabChange(null,3);
                    break;
                case tabNames[4]:
                    this.onTabChange(null,4);
                    break;
                case tabNames[5]:
                    this.onTabChange(null,5);
                    break;
                default:
                    this.onTabChange(null,0);
            }
        }
    };

    render() {
        const {
            informationTable,
            loading,
            refreshNeeded,
            selected,
            showAlert,
            showExternalRules,
            showExternalData,
            alertMessages
        } = this.state;
        
        const {
            project,
            serverBase
        } = this.props;

        return (
            <React.Fragment>
                <StyledTabs aria-label={"project tabs"} onChange={this.onTabChange} value={selected}>
                    <StyledLinkTab
                        label={"Data"}
                        to={{pathname: `/${project.id}/${tabNames[0]}`, state: {projectId: project.id}}}
                        {...this.getTabProps(0)}
                    />
                    <StyledLinkTab
                        label={
                            <OutdatedData invisible={!showAlert[0]} messages={alertMessages[0]}>
                                Dominance cones
                            </OutdatedData>
                        }
                        to={{pathname: `/${project.id}/${tabNames[1]}`, state: {projectId: project.id}}}
                        {...this.getTabProps(1)}
                        
                    />
                    <StyledLinkTab
                        label={
                            <OutdatedData invisible={!showAlert[1]} messages={alertMessages[1]}>
                                Class unions
                            </OutdatedData>
                        }
                        to={{pathname: `/${project.id}/${tabNames[2]}`, state: {projectId: project.id}}}
                        {...this.getTabProps(2)}
                        
                    />
                    <StyledLinkTab
                        icon={showExternalRules ?
                            <ExternalFile WrapperProps={{style: { marginBottom: 0, marginRight: 8}}} /> : null
                        }
                        label={
                            <OutdatedData invisible={!showAlert[2]} messages={alertMessages[2]}>
                                Rules
                            </OutdatedData>
                        }
                        to={{pathname: `/${project.id}/${tabNames[3]}`, state: {projectId: project.id}}}
                        {...this.getTabProps(3)}
                        
                    />
                    <StyledLinkTab
                        icon={showExternalData ?
                            <ExternalFile WrapperProps={{style: { marginBottom: 0, marginRight: 8}}} /> : null
                        }
                        label={
                            <OutdatedData invisible={!showAlert[3]} messages={alertMessages[3]}>
                                Classification
                            </OutdatedData>
                        }
                        to={{pathname: `/${project.id}/${tabNames[4]}`, state: {projectId: project.id}}}
                        {...this.getTabProps(4)}
                        
                    />
                    <StyledLinkTab
                        label={
                            <OutdatedData invisible={!showAlert[4]} messages={alertMessages[4]}>
                                Cross-Validation
                            </OutdatedData>
                        }
                        to={{pathname: `/${project.id}/${tabNames[5]}`, state: {projectId: project.id}}}
                        {...this.getTabProps(5)}
                        
                    />
                </StyledTabs>
                <Switch>
                {
                    {

                        0: <Route
                            path={`/${project.id}/${tabNames[0]}`}
                            render={() => <Data
                                informationTable={informationTable}
                                loading={loading}
                                onDataChange={this.onDataChange}
                                project={project}
                                refreshNeeded={refreshNeeded}
                                serverBase={serverBase}
                                updateProject={this.props.updateProject}/>
                            }
                        />,
                        1: <Route
                            path={`/${project.id}/${tabNames[1]}`}
                            render={() => <Cones {...this.getTabBodyProps(0)} />}
                        />,
                        2: <Route
                            path={`/${project.id}/${tabNames[2]}`}
                            render={() => <Unions {...this.getTabBodyProps(1)} />}
                        />,
                        3: <Route
                            path={`/${project.id}/${tabNames[3]}`}
                            render={() => <Rules
                                onDataUploaded={this.onDataUploaded}
                                onRulesUploaded={this.onRulesUploaded}
                                {...this.getTabBodyProps(2)}/>
                            }
                        />,
                        4: <Route
                            path={`/${project.id}/${tabNames[4]}`}
                            render={() => <Classification
                                onDataUploaded={this.onDataUploaded}
                                {...this.getTabBodyProps(3)}/>
                            }
                        />,
                        5: <Route
                            path={`/${project.id}/${tabNames[5]}`}
                            render={() => <CrossValidation {...this.getTabBodyProps(4)} />}
                        />
                    }[selected]
                }
                </Switch>
            </React.Fragment>
        );
    }
}

ProjectTabs.propTypes = {
    deleting: PropTypes.bool,
    objectGlobalName: PropTypes.string,
    onSnackbarOpen: PropTypes.func,
    project: PropTypes.object,
    serverBase: PropTypes.string,
    updateProject: PropTypes.func
};

export default ProjectTabs;