import React, {Component} from 'react';
import PropTypes from "prop-types";
import { nonNullProperty } from "../../../Utils/utilFunctions";
import { downloadRules, fetchRules } from "../../../Utils/utilFunctions/fetchFunctions";
import { parseFormData } from "../../../Utils/utilFunctions/fetchFunctions/parseFormData";
import { parseRulesItems } from "../../../Utils/utilFunctions/parseItems";
import { parseRulesListItems } from "../../../Utils/utilFunctions/parseListItems";
import { parseRulesParams } from "../../../Utils/utilFunctions/parseParams";
import TabBody from "../../../Utils/Containers/TabBody";
import filterFunction from "../Filtering/FilterFunction";
import FilterTextField from "../Filtering/FilterTextField";
import { CalculateButton, SettingsButton, SortButton, StyledIconButton } from "../../../Utils/Buttons";
import ThresholdSelector from "../Calculations/ThresholdSelector";
import TypeOfUnionsSelector from "../Calculations/TypeOfUnionsSelector";
import TypeOfRulesSelector from "../Calculations/TypeOfRulesSelector";
import CustomBox from "../../../Utils/Containers/CustomBox";
import CustomDrawer from "../../../Utils/Containers/CustomDrawer"
import StyledDivider from "../../../Utils/DataDisplay/StyledDivider";
import CustomTooltip from "../../../Utils/DataDisplay/CustomTooltip";
import CircleHelper from "../../../Utils/Feedback/CircleHelper";
import { RulesDialog } from "../../../Utils/Dialogs/DetailsDialog";
import StyledAlert from "../../../Utils/Feedback/StyledAlert";
import { createCategories, simpleSort, SortMenu } from "../../../Utils/Menus/SortMenu";
import CustomUpload from "../../../Utils/Inputs/CustomUpload";
import CustomHeader from "../../../Utils/Surfaces/CustomHeader";
import SvgIcon from "@material-ui/core/SvgIcon";
import FileUpload from "mdi-material-ui/FileUpload";
import SaveIcon from "@material-ui/icons/Save";
import { mdiTextBox } from '@mdi/js';
/**
* <h3>Overview</h3>
* The rules tab in RuLeStudio.
* Presents the list of all rules generated for information table from current project.
*
* @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.onDataUploaded - Callback fired when tab receives information that new data was uploaded.
* @param {function} props.onRulesUploaded - Callback fired when tab receives information that rule set 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 Rules extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
data: null,
items: null,
displayedItems: [],
parameters: {
consistencyThreshold: 0,
typeOfRules: "certain",
typeOfUnions: "monotonic"
},
parametersSaved: true,
selectedItem: null,
open: {
details: false,
settings: false,
},
sort: {
anchorE1: null,
order: "asc",
value: "id"
},
alertProps: undefined,
};
this.upperBar = React.createRef();
}
/**
* <h3>Overview</h3>
* Makes an API call on rules to receive current copy of rule set from server.
* Then, updates state and makes necessary changes in display.
*
* @function
* @memberOf Rules
*/
getRules = () => {
const { project, serverBase } = this.props;
const pathParams = { projectId: project.id };
const method = "GET";
fetchRules(
pathParams, method, null, serverBase
).then(result => {
if (result && this._isMounted) {
const items = parseRulesItems(result);
const resultParameters = result.hasOwnProperty("parameters") ?
parseRulesParams(result.parameters) : { };
this.setState(({parameters, alertProps}) => ({
data: result,
items: items,
displayedItems: items,
parameters: { ...parameters, ...resultParameters},
alertProps: result.hasOwnProperty("errorMessage") ?
{ message: result.errorMessage, open: true, severity: "error" } : alertProps
}));
if (result.hasOwnProperty("isCurrentData")) {
const messages = result.hasOwnProperty("errorMessages") ?
result.errorMessages : null;
this.props.showAlert(this.props.value, !result.isCurrentData, messages);
}
if (result.hasOwnProperty("externalRules")) {
this.props.onRulesUploaded(result.externalRules);
}
if (result.hasOwnProperty("validityRulesContainer")) {
this.updateAlerts(result.validityRulesContainer);
}
}
}).catch(exception => {
this.onSnackbarOpen(exception, () => {
if (this._isMounted) {
this.setState({
data: null,
items: null,
displayedItems: []
});
}
});
}).finally(() => {
if (this._isMounted) {
const { displayedItems } = this.state;
const { project: { parameters, parametersSaved, sortParams }} = this.props;
const { consistencyThreshold, typeOfRules, typeOfUnions } = parameters;
this.setState(({parameters, sort}) => ({
loading: false,
parameters: parametersSaved ?
parameters : { ...parameters, ...{ consistencyThreshold, typeOfRules, typeOfUnions } },
parametersSaved: parametersSaved,
sort: { ...sort, ...sortParams.rules },
selectedItem: null
}), () => this.onSortChange(displayedItems));
}
});
};
/**
* <h3>Overview</h3>
* A component's lifecycle method. Fired once when component was mounted.
*
* <h3>Goal</h3>
* Method calls {@link getRules}.
*
* @function
* @memberOf Rules
*/
componentDidMount() {
this._isMounted = true;
this.setState({ loading: true }, this.getRules);
}
/**
* <h3>Overview</h3>
* A component's lifecycle method. Fired after a component was updated.
*
* <h3>Goal</h3>
* If type of unions was changed to <code>monotonic</code> and consistency threshold is equal to 1,
* method changes value of threshold to 0.
* <br>
* <br>
* If type of rules was changed to <code>possible</code>, method changes consistency threshold to 0.
* <br>
* <br>
* If project was changed, method saves changes from previous project
* and calls {@link getRules} to receive the latest copy of rule set.
*
* @function
* @memberOf Rules
* @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) {
const { parameters: prevParameters } = prevState;
const { parameters } = this.state;
if (parameters.typeOfUnions !== 'monotonic') {
if (parameters.consistencyThreshold === 1) {
this.setState(({parameters}) => ({
parameters: { ...parameters, consistencyThreshold: 0, typeOfUnions: "monotonic" }
}));
} else {
this.setState(({parameters}) => ({
parameters: { ...parameters, typeOfUnions: "monotonic" }
}));
}
}
if (parameters.typeOfRules !== prevParameters.typeOfRules && parameters.typeOfRules === "possible") {
this.setState(({parameters}) => ({
parameters: { ...parameters, consistencyThreshold: 0 }
}));
}
if (prevProps.project.id !== this.props.project.id) {
const { parametersSaved, sort: { order, value } } = prevState;
let project = { ...prevProps.project };
project.sortParams.rules = { ...project.sortParams.rules, ...{ order, value } };
if (!parametersSaved) {
const { parameters } = prevState;
project.parameters = {
...project.parameters,
consistencyThreshold: parameters.consistencyThreshold,
typeOfRules: parameters.typeOfRules
};
project.parametersSaved = parametersSaved;
}
this.props.onTabChange(project);
this.setState({ loading: true }, this.getRules);
}
}
/**
* <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 Rules
*/
componentWillUnmount() {
this._isMounted = false;
const { parametersSaved , sort: { order, value } } = this.state;
let project = JSON.parse(JSON.stringify(this.props.project));
project.sortParams.rules = { ...project.sortParams.rules, ...{ order, value } };
if ( !parametersSaved ) {
const { parameters } = this.state;
project.parameters = {
...project.parameters,
consistencyThreshold: parameters.consistencyThreshold,
typeOfRules: parameters.typeOfRules
};
project.parametersSaved = parametersSaved;
}
this.props.onTabChange(project);
}
/**
* <h3>Overview</h3>
* Makes an API call on rules to generate new rule set from current information table and parameters.
* Then, updates state and makes necessary changes in display.
*
* @function
* @memberOf Rules
*/
onCalculateClick = () => {
const { project, serverBase } = this.props;
const { parameters } = this.state;
this.setState({
loading: true,
}, () => {
const pathParams = { projectId: project.id };
const method = "PUT";
const data = parseFormData(parameters, null);
fetchRules(
pathParams, method, data, serverBase
).then(result => {
if (result) {
if (this._isMounted) {
const items = parseRulesItems(result);
this.setState(({alertProps}) => ({
data: result,
items: items,
displayedItems: items,
parametersSaved: true,
alertProps: result.hasOwnProperty("errorMessage") ?
{ message: result.errorMessage, open: true, severity: "error" } : alertProps
}));
}
let projectCopy = JSON.parse(JSON.stringify(project));
const newParameters = result.hasOwnProperty("parameters") ?
parseRulesParams(result.parameters) : { };
projectCopy.parameters = {
...projectCopy.parameters,
consistencyThreshold: newParameters.consistencyThreshold,
typeOfRules: newParameters.typeOfRules
};
projectCopy.parametersSaved = true;
if (result.hasOwnProperty("isCurrentData")) {
const messages = result.hasOwnProperty("errorMessages") ?
result.errorMessages : null;
this.props.showAlert(this.props.value, !result.isCurrentData, messages);
}
if (result.hasOwnProperty("externalRules")) {
this.props.onRulesUploaded(result.externalRules);
}
if (result.hasOwnProperty("validityRulesContainer")) {
this.updateAlerts(result.validityRulesContainer);
}
this.props.onTabChange(projectCopy);
}
}).catch(exception => {
this.onSnackbarOpen(exception, () => {
if (this._isMounted) {
this.setState({
data: null,
items: null,
displayedItems: []
});
}
});
}).finally(() => {
const { displayedItems } = this.state;
if (this._isMounted) {
this.setState({
loading: false,
selectedItem: null
}, () => this.onSortChange(displayedItems));
}
});
});
};
/**
* <h3>Overview</h3>
* Makes an API call on rules to upload user's rule set.
* Then, updates states and makes necessary changes in display.
*
* @function
* @memberOf Rules
* @param {Object} event - Represents an event that takes place in DOM.
*/
onUploadFileChanged = (event) => {
if (event.target.files[0]) {
const { project, serverBase } = this.props;
const pathParams = { projectId: project.id };
const method = "PUT";
const data = parseFormData(null, { rules: event.target.files[0] });
this.setState({
loading: true,
}, () => {
fetchRules(
pathParams, method, data, serverBase, true
).then(result => {
if (result) {
if (this._isMounted) {
const items = parseRulesItems(result);
let alertProps = undefined;
if (result.hasOwnProperty("errorMessage")) {
alertProps = { message: result.errorMessage, open: true, severity: "error" };
}
this.setState({
data: result,
items: items,
displayedItems: items,
alertProps: alertProps
});
}
if (result.hasOwnProperty("isCurrentData")) {
const messages = result.hasOwnProperty("errorMessages") ?
result.errorMessages : null;
this.props.showAlert(this.props.value, !result.isCurrentData, messages);
}
if (result.hasOwnProperty("externalRules")) {
this.props.onRulesUploaded(result.externalRules);
}
if (result.hasOwnProperty("validityRulesContainer")) {
this.updateAlerts(result.validityRulesContainer);
}
}
}).catch(exception => {
this.onSnackbarOpen(exception, () => {
if (this._isMounted) {
this.setState({
data: null,
items: null,
displayedItems: []
});
}
});
}).finally(() => {
const { displayedItems } = this.state;
if (this._isMounted) {
this.setState({
loading: false,
selectedItem: null
}, () => this.onSortChange(displayedItems));
}
});
});
}
};
/**
* <h3>Overview</h3>
* Used when changes in {@link Rules} had an impact on results in {@link Unions} or {@link Classification}.
* Updates classification and unions in current project, makes necessary changes in display.
*
* @function
* @memberOf Rules
* @param {Object} validityRulesContainer - The part of response from server
*/
updateAlerts = (validityRulesContainer) => {
if (validityRulesContainer.classification != null) {
if (validityRulesContainer.classification.hasOwnProperty("isCurrentData")) {
const messages = validityRulesContainer.classification.hasOwnProperty("errorMessages") ?
validityRulesContainer.classification.errorMessages : null;
this.props.showAlert(this.props.value + 1, !validityRulesContainer.classification.isCurrentData, messages);
}
if (validityRulesContainer.classification.hasOwnProperty("externalData")) {
this.props.onDataUploaded(validityRulesContainer.classification.externalData);
}
}
if (validityRulesContainer.unions != null) {
if (validityRulesContainer.unions.hasOwnProperty("isCurrentData")) {
const messages = validityRulesContainer.unions.hasOwnProperty("errorMessages") ?
validityRulesContainer.unions.errorMessages : null;
this.props.showAlert(this.props.value - 1, !validityRulesContainer.unions.isCurrentData, messages);
}
}
};
/**
* <h3>Overview</h3>
* Makes an API call to download current rules set in XML format.
*
* @function
* @memberOf Rules
*/
onSaveRulesToXMLClick = () => {
const { project, serverBase } = this.props;
const pathParams = { projectId: project.id };
const queryParams = { format: "xml" };
downloadRules(pathParams, queryParams, serverBase)
.catch(this.onSnackbarOpen);
};
/**
* <h3>Overview</h3>
* Makes an API call to download current rule set in TXT format.
*
* @function
* @memberOf Rules
*/
onSaveRulesToTXTClick = () => {
const { project, serverBase } = this.props;
const pathParams = { projectId: project.id };
const queryParams = { format: "txt" };
downloadRules(pathParams, queryParams, serverBase)
.catch(this.onSnackbarOpen);
};
toggleOpen = (name) => {
this.setState(({open}) => ({
open: {...open, [name]: !open[name]}
}));
};
onDetailsOpen = (index) => {
const { items } = this.state;
this.setState(({open}) => ({
open: {...open, details: true, settings: false},
selectedItem: items[index]
}));
};
onConsistencyThresholdChange = (threshold) => {
const { loading } = this.state;
if (!loading) {
this.setState(({parameters}) => ({
parameters: {...parameters, consistencyThreshold: threshold},
parametersSaved: false
}));
}
};
onTypeOfRulesChange = (event) => {
const { loading } = this.state;
if (!loading) {
this.setState(({parameters}) => ({
parameters: {...parameters, typeOfRules: event.target.value},
parametersSaved: false
}));
}
};
onTypeOfUnionsChange = (event) => {
const { loading } = this.state;
if (!loading) {
this.setState(({parameters}) => ({
parameters: {...parameters, typeOfUnions: event.target.value},
parametersSaved: false
}));
}
};
/**
* <h3>Overview</h3>
* Filters items from {@link Rules}' state and then sorts them if any order was declared.
* Method uses {@link filterFunction} to filter items.
*
* @function
* @memberOf Rules
* @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) {
this.onSortChange(filterFunction(event.target.value.toString(), items.slice()));
}
};
onSortMenuOpen = (event) => {
const target = event.currentTarget;
this.setState(({sort}) => ({
sort: { ...sort, anchorE1: target }
}));
};
onSortMenuClose = () => {
this.setState(({sort}) => ({
sort: { ...sort, anchorE1: null }
}));
};
onSortValueChange = (event) => {
const value = event.target.value;
this.setState(({sort}) => ({
sort: { ...sort, value: value }
}), () => {
const { displayedItems } = this.state;
this.onSortChange(displayedItems);
});
};
onSortOrderChange = (event) => {
const order = event.target.value;
this.setState(({sort}) => ({
sort: { ...sort, order: order }
}), () => {
const { displayedItems } = this.state;
this.onSortChange(displayedItems);
});
};
/**
* <h3>Overview</h3>
* Sorts provided items and saves results in {@link Rules}' state.
* Method uses {@link simpleSort} function to sort items.
*
* @function
* @memberOf Rules
* @param {Object[]} items - A list of objects that will be sorted.
*/
onSortChange = (items) => {
if (items) {
const { items: originalItems, sort: { order, value } } = this.state;
let newItems = items.map(item => item.toSort(value));
newItems = simpleSort(newItems, value, order);
newItems = newItems.map(item => originalItems[item.id]);
this.setState({
displayedItems: newItems
});
} else {
this.setState({
displayedItems: null
});
}
};
onSnackbarOpen = (exception, setStateCallback) => {
if (!(exception.hasOwnProperty("type") && exception.type === "AlertError")) {
console.error(exception);
return;
}
this.setState({ alertProps: exception }, setStateCallback);
}
onSnackbarClose = (event, reason) => {
if (reason !== 'clickaway') {
this.setState(({alertProps}) => ({
alertProps: {...alertProps, open: false}
}));
}
};
render() {
const { loading, items, data, displayedItems, parameters, selectedItem, open, sort, alertProps } = this.state;
const { objectGlobalName, project: { id: projectId }, serverBase } = this.props;
const resultsExists = Array.isArray(items) && Boolean(items.length);
return (
<CustomBox id={"rules"} variant={"Tab"}>
<CustomDrawer
id={"rules-settings"}
open={open.settings}
onClose={() => this.toggleOpen("settings")}
placeholder={this.upperBar.current ? this.upperBar.current.offsetHeight : undefined}
>
<TypeOfRulesSelector
TextFieldProps={{
onChange: this.onTypeOfRulesChange,
value: parameters.typeOfRules
}}
/>
<TypeOfUnionsSelector
TextFieldProps={{
disabledChildren: ["standard"],
onChange: this.onTypeOfUnionsChange,
value: parameters.typeOfUnions
}}
variant={"extended"}
/>
<ThresholdSelector
keepChanges={parameters.typeOfRules !== 'possible'}
onChange={this.onConsistencyThresholdChange}
value={parameters.consistencyThreshold}
variant={"extended"}
/>
</CustomDrawer>
<CustomBox customScrollbar={true} id={"rules-content"} variant={"TabBody"}>
<CustomHeader id={"rules-header"} paperRef={this.upperBar}>
<SettingsButton onClick={() => this.toggleOpen("settings")} />
<StyledDivider margin={16} />
<CustomTooltip
disableMaxWidth={true}
title={"Click on settings button on the left to customize parameters"}
>
<CalculateButton
aria-label={"rules-calculate-button"}
disabled={loading}
onClick={this.onCalculateClick}
/>
</CustomTooltip>
<StyledDivider margin={16} />
<CustomTooltip title={"Upload file"}>
<CustomUpload
accept={".xml"}
disabled={loading}
id={"rules-upload-button"}
onChange={this.onUploadFileChanged}
>
<StyledIconButton
aria-label={"rules-upload-button"}
color={"primary"}
component={"span"}
disabled={loading}
>
<FileUpload />
</StyledIconButton>
</CustomUpload>
</CustomTooltip>
<CircleHelper
size={"smaller"}
title={"Attributes are taken from DATA."}
TooltipProps={{ placement: "bottom" }}
WrapperProps={{ style: { marginLeft: 16 }}}
/>
<StyledDivider margin={16} />
<CustomTooltip title={"Save rules to RuleML"}>
<StyledIconButton
aria-label={"rules-save-to-xml-button"}
color={"primary"}
disabled={!resultsExists || loading}
onClick={this.onSaveRulesToXMLClick}
>
<SaveIcon />
</StyledIconButton>
</CustomTooltip>
<StyledDivider margin={16} />
<CustomTooltip title={"Save rules to TXT"}>
<StyledIconButton
aria-label={"rules-save-to-txt-button"}
color={"primary"}
disabled={!resultsExists || loading}
onClick={this.onSaveRulesToTXTClick}
>
<SvgIcon><path d={mdiTextBox} /></SvgIcon>
</StyledIconButton>
</CustomTooltip>
<span style={{flexGrow: 1}} />
<SortButton
aria-controls={"rules-sort-menu"}
aria-haspopup={true}
aria-label={"sort-rules"}
disabled={!resultsExists || loading}
onClick={this.onSortMenuOpen}
invisible={sort.value === "id" && sort.order === "asc"}
tooltip={resultsExists ? "Sort rules" : "No content to sort"}
tooltipId={"rules-sort-button-tooltip"}
TooltipProps={{
WrapperProps: { style: { marginRight: "0.5rem" } }
}}
/>
<FilterTextField onChange={this.onFilterChange} />
</CustomHeader>
{resultsExists &&
<SortMenu
anchorE1={sort.anchorE1}
ContentProps={{
categories: createCategories(
Object.keys(items[0].traits).filter(value => value !== "Type"),
"none (default index)",
"id"
),
chooseOrder: true,
onCategoryChange: this.onSortValueChange,
onOrderChange: this.onSortOrderChange,
order: sort.order,
rowHeight: 28,
value: sort.value
}}
id={"rules-sort-menu"}
onClose={this.onSortMenuClose}
/>
}
<TabBody
content={parseRulesListItems(displayedItems)}
id={"rules-list"}
isArray={Array.isArray(displayedItems) && Boolean(displayedItems.length)}
isLoading={loading}
ListProps={{
onItemSelected: this.onDetailsOpen
}}
ListSubheaderProps={{
disableLeftGutter: true,
disableRightGutter: false,
helper: (
<p aria-label={"helper-text"} style={{margin: 0, textAlign: "justify"}}>
{
"First row of each rule presents decision condition. " +
"Next rows present subsequent elementary conditions. " +
"These elementary conditions are connected by AND. " +
"Last row shows chosen rule’s characteristics."
}
</p>
),
style: this.upperBar.current ? { top: this.upperBar.current.offsetHeight } : undefined
}}
noFilterResults={!displayedItems}
subheaderContent={[
{
label: "Number of rules:",
value: Array.isArray(displayedItems) ? displayedItems.length : "-"
},
{
label: "Calculated in:",
value: nonNullProperty(data, "calculationsTime") ?
data.calculationsTime : "-"
}
]}
/>
{selectedItem !== null &&
<RulesDialog
item={selectedItem}
objectGlobalName={objectGlobalName}
onClose={() => this.toggleOpen("details")}
onSnackbarOpen={this.onSnackbarOpen}
open={open.details}
projectId={projectId}
serverBase={serverBase}
/>
}
</CustomBox>
<StyledAlert {...alertProps} onClose={this.onSnackbarClose} />
</CustomBox>
)
}
}
Rules.propTypes = {
objectGlobalName: PropTypes.string,
onDataUploaded: PropTypes.func,
onRulesUploaded: PropTypes.func,
onTabChange: PropTypes.func,
project: PropTypes.object,
serverBase: PropTypes.string,
showAlert: PropTypes.func,
value: PropTypes.number
};
export default Rules;
Source