<commit_before>/*
 * Copyright 2015 Mirantis, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may obtain
 * a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 **/
define(
[
    'jquery',
    'underscore',
    'backbone',
    'react',
    'i18n',
    'utils',
    'models',
    'dispatcher',
    'views/dialogs',
    'views/controls',
    'component_mixins',
    'react-dnd',
    'views/cluster_page_tabs/nodes_tab_screens/offloading_modes_control'
],
function($, _, Backbone, React, i18n, utils, models, dispatcher, dialogs, controls, ComponentMixins, DND, OffloadingModes) {
    'use strict';

    var ns = 'cluster_page.nodes_tab.configure_interfaces.';

    var EditNodeInterfacesScreen = React.createClass({
        mixins: [
            ComponentMixins.backboneMixin('interfaces', 'change reset update'),
            ComponentMixins.backboneMixin('cluster'),
            ComponentMixins.backboneMixin('nodes', 'change reset update'),
            ComponentMixins.unsavedChangesMixin
        ],
        statics: {
            fetchData: function(options) {
                var cluster = options.cluster,
                    nodes = utils.getNodeListFromTabOptions(options);

                if (!nodes || !nodes.areInterfacesConfigurable()) {
                    return $.Deferred().reject();
                }

                var networkConfiguration = cluster.get('networkConfiguration'),
                    networksMetadata = new models.ReleaseNetworkProperties();

                return $.when(...nodes.map(function(node) {
                    node.interfaces = new models.Interfaces();
                    return node.interfaces.fetch({
                        url: _.result(node, 'url') + '/interfaces',
                        reset: true
                    }, this);
                }, this).concat([
                    networkConfiguration.fetch({cache: true}),
                    networksMetadata.fetch({
                        url: '/api/releases/' + cluster.get('release_id') + '/networks'
                    })]))
                    .then(_.bind(function() {
                        var interfaces = new models.Interfaces();
                        interfaces.set(_.cloneDeep(nodes.at(0).interfaces.toJSON()), {parse: true});
                        return {
                            interfaces: interfaces,
                            nodes: nodes,
                            bondingConfig: networksMetadata.get('bonding'),
                            configModels: {
                                version: app.version,
                                cluster: cluster,
                                settings: cluster.get('settings')
                            }
                        };
                    }, this));
            }
        },
        getInitialState: function() {
            return {
                actionInProgress: false,
                interfaceErrors: {},
                interfacesMap: _.range(0, this.props.interfaces.length)
            };
        },
        componentWillMount: function() {
            this.setState({initialInterfaces: _.cloneDeep(this.interfacesToJSON(this.props.interfaces))});
        },
        componentDidMount: function() {
            this.validate();
        },
        isLocked: function() {
            return !!this.props.cluster.task({group: 'deployment', active: true}) ||
                !_.all(this.props.nodes.invoke('areInterfacesConfigurable'));
        },
        initInterfacesMap: function() {
            this.setState({interfacesMap: _.range(0, this.props.interfaces.length)});
        },
        calculateInterfacesMap: function() {
/*
            this.props.interfaces contains interfaces configuration that user is changing on UI.
            node.interfaces - actual interfaces configuration.

            When one bonds some interfaces only this.props.interfaces gets changed
            (new bond-interface is added at the beginning). Thus it's no longer possible to
            reference node.interfaces by index from this.props.interfaces.

            interfacesMap array maps indexes from this.props.interfaces to node.interfaces
            until changes are applied.
*/
            var firstNodeInterfaces = this.props.nodes.at(0).interfaces;
            this.setState({interfacesMap: this.props.interfaces.map(
                function(ifc) {
                    if (ifc.isBond()) return null;
                    return firstNodeInterfaces.indexOf(
                        firstNodeInterfaces.findWhere({name: ifc.get('name')})
                    );
                })
            });
        },
        interfacesPickFromJSON: function(json) {
            // Pick certain interface fields that have influence on hasChanges.
            return _.pick(json, ['assigned_networks', 'mode', 'type', 'slaves', 'bond_properties', 'interface_properties', 'offloading_modes']);
        },
        interfacesToJSON: function(interfaces, remainingNodesMode) {
            // Sometimes 'state' is sent from the API and sometimes not
            // It's better to just unify all inputs to the one without state.
            var picker = remainingNodesMode ? this.interfacesPickFromJSON : function(json) { return _.omit(json, 'state'); };

            return interfaces.map(function(ifc) {
                return picker(ifc.toJSON());
            });
        },
        hasChangesInRemainingNodes: function() {
            var initialInterfacesOmitted = _.map(this.state.initialInterfaces, this.interfacesPickFromJSON);

            return _.any(this.props.nodes.slice(1), _.bind(function(node) {
                return !_.isEqual(initialInterfacesOmitted, this.interfacesToJSON(node.interfaces, true));
            }, this));
        },
        hasChanges: function() {
            return !this.isLocked() && (!_.isEqual(this.state.initialInterfaces, this.interfacesToJSON(this.props.interfaces)) ||
                this.hasChangesInRemainingNodes());
        },
        loadDefaults: function() {
            this.setState({actionInProgress: true});
            $.when(this.props.interfaces.fetch({
                url: _.result(this.props.nodes.at(0), 'url') + '/interfaces/default_assignment', reset: true
            }, this)).done(_.bind(function() {
                this.calculateInterfacesMap();
                this.setState({actionInProgress: false});
            }, this)).fail(_.bind(function(response) {
                var errorNS = ns + 'configuration_error.';
                utils.showErrorDialog({
                    title: i18n(errorNS + 'title'),
                    message: i18n(errorNS + 'load_defaults_warning'),
                    response: response
                });
            }, this));
        },
        revertChanges: function() {
            this.props.interfaces.reset(_.cloneDeep(this.state.initialInterfaces), {parse: true});
            this.initInterfacesMap();
        },
        applyChanges: function() {
            if (!this.isSavingPossible()) return $.Deferred().reject();

            var nodes = this.props.nodes,
                interfaces = this.props.interfaces,
                bonds = interfaces.filter(function(ifc) {return ifc.isBond();}),
                bondsByName = bonds.reduce(function(result, bond) {
                        result[bond.get('name')] = bond;
                        return result;
                    }, {});

            // bonding map contains indexes of slave interfaces
            // it is needed to build the same configuration for all the nodes
            // as interface names might be different, so we use indexes
            var bondingMap = _.map(bonds, function(bond) {
                return _.map(bond.get('slaves'), function(slave) {
                    return interfaces.indexOf(interfaces.findWhere(slave));
                });
            });
            this.setState({actionInProgress: true});
            return $.when(...nodes.map(function(node) {
                var oldNodeBonds, nodeBonds;
                // removing previously configured bonds
                oldNodeBonds = node.interfaces.filter(function(ifc) {return ifc.isBond();});
                node.interfaces.remove(oldNodeBonds);
                // creating node-specific bonds without slaves
                nodeBonds = _.map(bonds, function(bond) {
                    return new models.Interface(_.omit(bond.toJSON(), 'slaves'), {parse: true});
                }, this);
                node.interfaces.add(nodeBonds);
                // determining slaves using bonding map
                _.each(nodeBonds, function(bond, bondIndex) {
                    var slaveIndexes = bondingMap[bondIndex],
                        slaveInterfaces = _.map(slaveIndexes, node.interfaces.at, node.interfaces);
                    bond.set({slaves: _.invoke(slaveInterfaces, 'pick', 'name')});
                });

                // Assigning networks according to user choice and interface properties
                node.interfaces.each(function(ifc, index) {
                    var updatedIfc = ifc.isBond() ? bondsByName[ifc.get('name')] : interfaces.at(index);
                    ifc.set({<commit_msg>Fix changes calculation on node interface configuration screen<commit_after>/*
 * Copyright 2015 Mirantis, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may obtain
 * a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 **/
define(
[
    'jquery',
    'underscore',
    'backbone',
    'react',
    'i18n',
    'utils',
    'models',
    'dispatcher',
    'views/dialogs',
    'views/controls',
    'component_mixins',
    'react-dnd',
    'views/cluster_page_tabs/nodes_tab_screens/offloading_modes_control'
],
function($, _, Backbone, React, i18n, utils, models, dispatcher, dialogs, controls, ComponentMixins, DND, OffloadingModes) {
    'use strict';

    var ns = 'cluster_page.nodes_tab.configure_interfaces.';

    var EditNodeInterfacesScreen = React.createClass({
        mixins: [
            ComponentMixins.backboneMixin('interfaces', 'change reset update'),
            ComponentMixins.backboneMixin('cluster'),
            ComponentMixins.backboneMixin('nodes', 'change reset update'),
            ComponentMixins.unsavedChangesMixin
        ],
        statics: {
            fetchData: function(options) {
                var cluster = options.cluster,
                    nodes = utils.getNodeListFromTabOptions(options);

                if (!nodes || !nodes.areInterfacesConfigurable()) {
                    return $.Deferred().reject();
                }

                var networkConfiguration = cluster.get('networkConfiguration'),
                    networksMetadata = new models.ReleaseNetworkProperties();

                return $.when(...nodes.map(function(node) {
                    node.interfaces = new models.Interfaces();
                    return node.interfaces.fetch({
                        url: _.result(node, 'url') + '/interfaces',
                        reset: true
                    }, this);
                }, this).concat([
                    networkConfiguration.fetch({cache: true}),
                    networksMetadata.fetch({
                        url: '/api/releases/' + cluster.get('release_id') + '/networks'
                    })]))
                    .then(_.bind(function() {
                        var interfaces = new models.Interfaces();
                        interfaces.set(_.cloneDeep(nodes.at(0).interfaces.toJSON()), {parse: true});
                        return {
                            interfaces: interfaces,
                            nodes: nodes,
                            bondingConfig: networksMetadata.get('bonding'),
                            configModels: {
                                version: app.version,
                                cluster: cluster,
                                settings: cluster.get('settings')
                            }
                        };
                    }, this));
            }
        },
        getInitialState: function() {
            return {
                actionInProgress: false,
                interfaceErrors: {},
                interfacesMap: _.range(0, this.props.interfaces.length)
            };
        },
        componentWillMount: function() {
            this.setState({initialInterfaces: _.cloneDeep(this.interfacesToJSON(this.props.interfaces))});
        },
        componentDidMount: function() {
            this.validate();
        },
        isLocked: function() {
            return !!this.props.cluster.task({group: 'deployment', active: true}) ||
                !_.all(this.props.nodes.invoke('areInterfacesConfigurable'));
        },
        initInterfacesMap: function() {
            this.setState({interfacesMap: _.range(0, this.props.interfaces.length)});
        },
        calculateInterfacesMap: function() {
/*
            this.props.interfaces contains interfaces configuration that user is changing on UI.
            node.interfaces - actual interfaces configuration.

            When one bonds some interfaces only this.props.interfaces gets changed
            (new bond-interface is added at the beginning). Thus it's no longer possible to
            reference node.interfaces by index from this.props.interfaces.

            interfacesMap array maps indexes from this.props.interfaces to node.interfaces
            until changes are applied.
*/
            var firstNodeInterfaces = this.props.nodes.at(0).interfaces;
            this.setState({interfacesMap: this.props.interfaces.map(
                function(ifc) {
                    if (ifc.isBond()) return null;
                    return firstNodeInterfaces.indexOf(
                        firstNodeInterfaces.findWhere({name: ifc.get('name')})
                    );
                })
            });
        },
        interfacesPickFromJSON: function(json) {
            // Pick certain interface fields that have influence on hasChanges.
            return _.pick(json, ['assigned_networks', 'mode', 'type', 'slaves', 'bond_properties', 'interface_properties', 'offloading_modes']);
        },
        interfacesToJSON: function(interfaces, remainingNodesMode) {
            // Sometimes 'state' is sent from the API and sometimes not
            // It's better to just unify all inputs to the one without state.
            var picker = remainingNodesMode ? this.interfacesPickFromJSON : (json) => _.omit(json, 'state');
            return interfaces.map((ifc) => picker(ifc.toJSON()));
        },
        hasChangesInRemainingNodes: function() {
            var initialInterfacesData = _.map(this.state.initialInterfaces, this.interfacesPickFromJSON);
            return _.any(this.props.nodes.slice(1), (node) => {
                var interfacesData = this.interfacesToJSON(node.interfaces, true);
                return _.any(initialInterfacesData, (ifcData, index) => {
                    return _.any(ifcData, (data, attribute) => {
                        if (attribute == 'slaves') {
                            // bond 'slaves' attribute contains information about slave name only
                            // but interface names can be different between nodes
                            // and can not be used for the comparison
                            return data.length != (interfacesData[index].slaves || {}).length;
                        }
                        return !_.isEqual(data, interfacesData[index][attribute]);
                    });
                });
            });
        },
        hasChanges: function() {
            return !this.isLocked() &&
                (!_.isEqual(this.state.initialInterfaces, this.interfacesToJSON(this.props.interfaces)) ||
                this.props.nodes.length > 1 && this.hasChangesInRemainingNodes());
        },
        loadDefaults: function() {
            this.setState({actionInProgress: true});
            $.when(this.props.interfaces.fetch({
                url: _.result(this.props.nodes.at(0), 'url') + '/interfaces/default_assignment', reset: true
            }, this)).done(_.bind(function() {
                this.calculateInterfacesMap();
                this.setState({actionInProgress: false});
            }, this)).fail(_.bind(function(response) {
                var errorNS = ns + 'configuration_error.';
                utils.showErrorDialog({
                    title: i18n(errorNS + 'title'),
                    message: i18n(errorNS + 'load_defaults_warning'),
                    response: response
                });
            }, this));
        },
        revertChanges: function() {
            this.props.interfaces.reset(_.cloneDeep(this.state.initialInterfaces), {parse: true});
            this.initInterfacesMap();
        },
        applyChanges: function() {
            if (!this.isSavingPossible()) return $.Deferred().reject();

            var nodes = this.props.nodes,
                interfaces = this.props.interfaces,
                bonds = interfaces.filter((ifc) => ifc.isBond()),
                bondsByName = bonds.reduce((result, bond) => {
                        result[bond.get('name')] = bond;
                        return result;
                    }, {});

            // bonding map contains indexes of slave interfaces
            // it is needed to build the same configuration for all the nodes
            // as interface names might be different, so we use indexes
            var bondingMap = _.map(bonds,
                (bond) => _.map(bond.get('slaves'), (slave) => interfaces.indexOf(interfaces.find(slave)))
            );
            this.setState({actionInProgress: true});
            return $.when(...nodes.map(function(node) {
                var oldNodeBonds, nodeBonds;
                // removing previously configured bonds
                oldNodeBonds = node.interfaces.filter(function(ifc) {return ifc.isBond();});
                node.interfaces.remove(oldNodeBonds);
                // creating node-specific bonds without slaves
                nodeBonds = _.map(bonds, function(bond) {
                    return new models.Interface(_.omit(bond.toJSON(), 'slaves'), {parse: true});
                }, this);
                node.interfaces.add(nodeBonds);
                // determining slaves using bonding map
                _.each(nodeBonds, function(bond, bondIndex) {
                    var slaveIndexes = bondingMap[bondIndex],
                        slaveInterfaces = _.map(slaveIndexes, node.interfaces.at, node.interfaces);
                    bond.set({slaves: _.invoke(slaveInterfaces, 'pick', 'name')});
                });

                // Assigning networks according to user choice and interface properties
                node.interfaces.each(function(ifc, index) {
                    var updatedIfc = ifc.isBond() ? bondsByName[ifc.get('name')] : interfaces.at(index);
                    ifc.set({
{'fraction_non_alphanumeric': 0.10980649062688974, 'fraction_numerical': 0.0022676879661358597, 'mean_word_length': 1.7751363445671935, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2375085', 'n_tokens_mistral': 4992, 'n_tokens_neox': 4745, 'n_words': 1111}