<commit_before>/* global define */

define([
    'underscore',
    'highcharts'
], function(_, Highcharts) {

    var getColor = function(idx) {
        var colors = Highcharts.getOptions().colors;
        return colors[idx % colors.length];
    };

    var parseDate = function(str) {
        var year = parseInt(str.substr(0, 4));
        var month = parseInt(str.substr(5, 2)) -1;
        var day = parseInt(str.substr(8, 2));

        return Date.UTC(year, month, day);
    };

    var processResponse = function(resp, fields, seriesIdx) {
        if (fields.length > 2) {
            throw new Error('3-dimensional charts are not supported.' +
                            'Specify which field the series applies to.');
        }
        else if (!fields) {
            throw new Error('The field instances must be supplied');
        }

        var xLabels = [],
            yLabels = [];

        // Check whether the y-axis consists of enumerable data, otherwise
        // fallback to true for standard counts.
        var xField = fields[0],
            yField = fields[1];

        var xName, xEnum, xType, yName, yEnum, yType, chartType;

        xName = xField.get('name');
        xEnum = xField.get('enumerable') ||
            xField.get('simple_type') === 'boolean';

        if (xField.get('simple_type') === 'date' ||
                xField.get('simple_type') === 'datetime') {
            xType = 'datetime';
        }
        else {
            xType = 'linear';
        }

        if (yField) {
            yName = yField.get('name');
            yEnum = yField.get('enumerable') ||
                yField.get('simple_type') === 'boolean';

            if (yField.get('simple_type') === 'date' ||
                    yField.get('simple_type') === 'datetime') {
                yType = 'datetime';
            }
            else {
                yType = 'linear';
            }
        }
        else {
            yName = 'Frequency';
            yEnum = false;
            yType = 'linear';
        }

        if (xEnum && yEnum) {
            chartType = 'scatter';
            xLabels.push(' ');
            yLabels.push(' ');
        }
        else if (yField && !xEnum && !yEnum) {
            chartType = 'scatter';
        }
        else if (yEnum) {
            chartType = 'scatter';
        }
        else {
            chartType = 'column';
        }

        var seriesData = {},
            clustered = resp.clustered,
            respData = resp.data,
            point,
            svalue,
            series;

        // Perform the group by first prior to building each series.
        for (var i = 0; i < respData.length; i++) {
            point = respData[i];

            if (seriesIdx) {
                svalue = point.values.slice(seriesIdx, seriesIdx + 1)[0];
            }
            else {
                svalue = '';
            }

            if (!(series = seriesData[svalue])) {
                series = seriesData[svalue] = {
                    name: svalue,
                    stats: {
                        min: point.count,
                        max: point.count,
                        sum: point.count
                    }
                };

                if (xEnum && yEnum) {
                    series.data = [{x: 0, y: 0, radius: 0, sentinel: true}, point];
                }
                else {
                    series.data = [point];
                }
            }
            else {
                // Add it to the corresponding seriesData.
                series.data.push(point);
                series.stats.min = Math.min(series.stats.min, point.count);
                series.stats.max = Math.max(series.stats.max, point.count);
                series.stats.sum += point.count;
            }

            var x = point.values[0];
            if (x === null) {
                x = '(no data)';
            }

            if (xEnum) {
                if (xLabels.indexOf(x.label) === -1) {
                    x = xLabels.push(x.label) - 1;
                }
            }
            else if (xType === 'datetime') {
                x = parseDate(x.value);
            }
            else {
                x = x.value;
            }

            var y;
            if (yField) {
                y = point.values[1];

                if (y === null) {
                    y = '(no data)';
                }

                if (yEnum) {
                    if (yLabels.indexOf(y.label) === -1) { // jshint ignore:line
                        y = yLabels.push(y.label) - 1;
                    }
                }
                else if (yType === 'datetime') {
                    y = parseDate(y.value);
                }
                else {
                    y = y.value;
                }
            }
            else {
                y = point.count;
            }

            point.x = x;
            point.y = y;
        }

        var seriesList = [];

        var xlen, ylen;

        if (xEnum && yEnum) {
            xlen = xLabels.push(' ') - 1;
            ylen = yLabels.push(' ') - 1;
        }

        var seriesNo = 0;
        for (var name in seriesData) {
            series = seriesData[name];

            if (xEnum && yEnum) {
                series.data.push({
                    x: 0,
                    y: ylen,
                    radius: 0,
                    sentinel: true
                });

                series.data.push({
                    x: xlen,
                    y: ylen,
                    radius: 0,
                    sentinel: true
                });

                series.data.push({
                    x: xlen,
                    y: ylen,
                    radius: 0,
                    sentinel: true
                });
            }

            seriesList.push(series);

            var avg = series.stats.avg = series.stats.sum /
                parseFloat(series.data.length, 10);

            if (chartType === 'scatter') {
                for (var i = 0; i < series.data.length; i++) { // jshint ignore:line
                    var p = series.data[i];

                    if (p.sentinel) { // jshint ignore:line
                        continue;
                    }

                    var norm = Math.min(Math.max(parseInt(
                                    parseFloat(p.count, 10) / avg * 5) / 10, 0.05), 1);
                    var color = Highcharts.Color(getColor(seriesNo)).setOpacity(norm);

                    p.marker = {
                        fillColor: color.get()
                    };

                    if (xEnum) { // jshint ignore:line
                        p.marker.radius = 7;
                    }
                }
            }

            seriesNo++;
        }

        // TODO: This is really ugly, clean up the code that creates and
        // returns the formatter.
        var formatterFunc;
        if (chartType === 'scatter' && xEnum) {
            // Multiple series
            if (seriesList[1]) {
                formatterFunc = function () {
                    return '
' + this.series.name + '

' + xName +
                           ':' + this.series.xAxis.categories[this.point.x] +
                           '
' + yName + ':' +
                           this.series.yAxis.categories[this.point.y];
                };
            }
            else {
                formatterFunc = function() {
                    return '' + xName + ':' +
                           this.series.xAxis.categories[this.point.x] + '
' +
                           yName + ':' + this.series.yAxis.categories[this.point.y];
                };
            }
        }
        else if (chartType === 'column' && xEnum) {
            // Multiple series
            if (seriesList[1]) {
               formatterFunc = function() {
                    return '
' + this.series.name + '

' + xName +
                           ':' + this.series.xAxis.categories[this.point.x] +
                           '
' + yName + ':' +
                           Highcharts.numberFormat(parseFloat(this.y));
                };
            }
            else {
                formatterFunc = function() {
                    return '' + xName + ':' +
                           this.series.xAxis.categories[this.point.x] + '
' +
                           yName + ':' + Highcharts.numberFormat(parseFloat(this.y));
                };
            }
        }
        else {
            // Multiple series
            if (seriesList[1]) {
                formatterFunc = function() {
                    return '
' + this.series.name + '

' + xName +
                           ':' + Highcharts.numberFormat(parseFloat(this.x)) +
                           '
' + yName + ':' +
                           Highcharts.numberFormat(parseFloat(this.y));
                };
            }
            else {
                formatterFunc = function() {
                    return '' + xName + ':' +
                           Highcharts.numberFormat(parseFloat(this.x)) + '
' +
                           yName + ': ' + Highcharts.numberFormat(parseFloat(this.y));
                };
            }
        }

        var options = {
            clustered: clustered,
            chart: {
                type: chartType
            },
            title: {
                text: yField ? '' + xName + ' vs. ' + yName : '' + xName + ' ' + yName
            },
            series: seriesList,
            xAxis: {
                title: {
                  text: xName
                },
                type: xType
            },
            yAxis: {
                title: {
                  text: yName
                },
                type: yType
            },
            tooltip: {
                formatter: formatterFunc
            }
        };

        if (xLabels.length) {
            options.xAxis.categories = xLabels;
        }

        if (yLabels.length) {
            options.yAxis.categories = yLabels;
        }

        if (!seriesList[1]) {
            options.legend = {enabled: false};
        }

        if (chartType === 'scatter') {
            options.yAxis.gridLineWidth = 0;

            if (!xEnum) {
                options.chart.zoomType = 'xy';
            }
        }

        return options;
    };

    // Simple parser to format data for use by Highcharts.
    var getSeries = function(data) {
        var points = [],
            point,
            datum;

        for (var i = 0; i < data.length; i++) {
            datum = data[i];

            point = _.clone(datum);
            point.x = datum.values[0];

            if (datum.values[1] !== null) {
                point.y = datum.values[1];
            }
            else {
                point.y = datum.count;
            }

            points.push(point);
        }

        return {data: _.sortBy(points, 'x')};
    };

    return {
        processResponse: processResponse,
        getSeries: getSeries
    };

});
<commit_msg>Protect against malformed date strings in parseDate<commit_after>/* global define */

define([
    'underscore',
    'highcharts'
], function(_, Highcharts) {

    var getColor = function(idx) {
        var colors = Highcharts.getOptions().colors;
        return colors[idx % colors.length];
    };

    var parseDate = function(str) {
        // Short-circut if the string is empty or if it does not conform to
        // the expected YYYY-MM-DD format.
        if (!str || str.length != 10) return;

        var year = parseInt(str.substr(0, 4));
        var month = parseInt(str.substr(5, 2)) -1;
        var day = parseInt(str.substr(8, 2));

        return Date.UTC(year, month, day);
    };

    var processResponse = function(resp, fields, seriesIdx) {
        if (fields.length > 2) {
            throw new Error('3-dimensional charts are not supported.' +
                            'Specify which field the series applies to.');
        }
        else if (!fields) {
            throw new Error('The field instances must be supplied');
        }

        var xLabels = [],
            yLabels = [];

        // Check whether the y-axis consists of enumerable data, otherwise
        // fallback to true for standard counts.
        var xField = fields[0],
            yField = fields[1];

        var xName, xEnum, xType, yName, yEnum, yType, chartType;

        xName = xField.get('name');
        xEnum = xField.get('enumerable') ||
            xField.get('simple_type') === 'boolean';

        if (xField.get('simple_type') === 'date' ||
                xField.get('simple_type') === 'datetime') {
            xType = 'datetime';
        }
        else {
            xType = 'linear';
        }

        if (yField) {
            yName = yField.get('name');
            yEnum = yField.get('enumerable') ||
                yField.get('simple_type') === 'boolean';

            if (yField.get('simple_type') === 'date' ||
                    yField.get('simple_type') === 'datetime') {
                yType = 'datetime';
            }
            else {
                yType = 'linear';
            }
        }
        else {
            yName = 'Frequency';
            yEnum = false;
            yType = 'linear';
        }

        if (xEnum && yEnum) {
            chartType = 'scatter';
            xLabels.push(' ');
            yLabels.push(' ');
        }
        else if (yField && !xEnum && !yEnum) {
            chartType = 'scatter';
        }
        else if (yEnum) {
            chartType = 'scatter';
        }
        else {
            chartType = 'column';
        }

        var seriesData = {},
            clustered = resp.clustered,
            respData = resp.data,
            point,
            svalue,
            series;

        // Perform the group by first prior to building each series.
        for (var i = 0; i < respData.length; i++) {
            point = respData[i];

            if (seriesIdx) {
                svalue = point.values.slice(seriesIdx, seriesIdx + 1)[0];
            }
            else {
                svalue = '';
            }

            if (!(series = seriesData[svalue])) {
                series = seriesData[svalue] = {
                    name: svalue,
                    stats: {
                        min: point.count,
                        max: point.count,
                        sum: point.count
                    }
                };

                if (xEnum && yEnum) {
                    series.data = [{x: 0, y: 0, radius: 0, sentinel: true}, point];
                }
                else {
                    series.data = [point];
                }
            }
            else {
                // Add it to the corresponding seriesData.
                series.data.push(point);
                series.stats.min = Math.min(series.stats.min, point.count);
                series.stats.max = Math.max(series.stats.max, point.count);
                series.stats.sum += point.count;
            }

            var x = point.values[0];
            if (x === null) {
                x = '(no data)';
            }

            if (xEnum) {
                if (xLabels.indexOf(x.label) === -1) {
                    x = xLabels.push(x.label) - 1;
                }
            }
            else if (xType === 'datetime') {
                x = parseDate(x.value);
            }
            else {
                x = x.value;
            }

            var y;
            if (yField) {
                y = point.values[1];

                if (y === null) {
                    y = '(no data)';
                }

                if (yEnum) {
                    if (yLabels.indexOf(y.label) === -1) { // jshint ignore:line
                        y = yLabels.push(y.label) - 1;
                    }
                }
                else if (yType === 'datetime') {
                    y = parseDate(y.value);
                }
                else {
                    y = y.value;
                }
            }
            else {
                y = point.count;
            }

            point.x = x;
            point.y = y;
        }

        var seriesList = [];

        var xlen, ylen;

        if (xEnum && yEnum) {
            xlen = xLabels.push(' ') - 1;
            ylen = yLabels.push(' ') - 1;
        }

        var seriesNo = 0;
        for (var name in seriesData) {
            series = seriesData[name];

            if (xEnum && yEnum) {
                series.data.push({
                    x: 0,
                    y: ylen,
                    radius: 0,
                    sentinel: true
                });

                series.data.push({
                    x: xlen,
                    y: ylen,
                    radius: 0,
                    sentinel: true
                });

                series.data.push({
                    x: xlen,
                    y: ylen,
                    radius: 0,
                    sentinel: true
                });
            }

            seriesList.push(series);

            var avg = series.stats.avg = series.stats.sum /
                parseFloat(series.data.length, 10);

            if (chartType === 'scatter') {
                for (var i = 0; i < series.data.length; i++) { // jshint ignore:line
                    var p = series.data[i];

                    if (p.sentinel) { // jshint ignore:line
                        continue;
                    }

                    var norm = Math.min(Math.max(parseInt(
                                    parseFloat(p.count, 10) / avg * 5) / 10, 0.05), 1);
                    var color = Highcharts.Color(getColor(seriesNo)).setOpacity(norm);

                    p.marker = {
                        fillColor: color.get()
                    };

                    if (xEnum) { // jshint ignore:line
                        p.marker.radius = 7;
                    }
                }
            }

            seriesNo++;
        }

        // TODO: This is really ugly, clean up the code that creates and
        // returns the formatter.
        var formatterFunc;
        if (chartType === 'scatter' && xEnum) {
            // Multiple series
            if (seriesList[1]) {
                formatterFunc = function () {
                    return '
' + this.series.name + '

' + xName +
                           ':' + this.series.xAxis.categories[this.point.x] +
                           '
' + yName + ':' +
                           this.series.yAxis.categories[this.point.y];
                };
            }
            else {
                formatterFunc = function() {
                    return '' + xName + ':' +
                           this.series.xAxis.categories[this.point.x] + '
' +
                           yName + ':' + this.series.yAxis.categories[this.point.y];
                };
            }
        }
        else if (chartType === 'column' && xEnum) {
            // Multiple series
            if (seriesList[1]) {
               formatterFunc = function() {
                    return '
' + this.series.name + '

' + xName +
                           ':' + this.series.xAxis.categories[this.point.x] +
                           '
' + yName + ':' +
                           Highcharts.numberFormat(parseFloat(this.y));
                };
            }
            else {
                formatterFunc = function() {
                    return '' + xName + ':' +
                           this.series.xAxis.categories[this.point.x] + '
' +
                           yName + ':' + Highcharts.numberFormat(parseFloat(this.y));
                };
            }
        }
        else {
            // Multiple series
            if (seriesList[1]) {
                formatterFunc = function() {
                    return '
' + this.series.name + '

' + xName +
                           ':' + Highcharts.numberFormat(parseFloat(this.x)) +
                           '
' + yName + ':' +
                           Highcharts.numberFormat(parseFloat(this.y));
                };
            }
            else {
                formatterFunc = function() {
                    return '' + xName + ':' +
                           Highcharts.numberFormat(parseFloat(this.x)) + '
' +
                           yName + ': ' + Highcharts.numberFormat(parseFloat(this.y));
                };
            }
        }

        var options = {
            clustered: clustered,
            chart: {
                type: chartType
            },
            title: {
                text: yField ? '' + xName + ' vs. ' + yName : '' + xName + ' ' + yName
            },
            series: seriesList,
            xAxis: {
                title: {
                  text: xName
                },
                type: xType
            },
            yAxis: {
                title: {
                  text: yName
                },
                type: yType
            },
            tooltip: {
                formatter: formatterFunc
            }
        };

        if (xLabels.length) {
            options.xAxis.categories = xLabels;
        }

        if (yLabels.length) {
            options.yAxis.categories = yLabels;
        }

        if (!seriesList[1]) {
            options.legend = {enabled: false};
        }

        if (chartType === 'scatter') {
            options.yAxis.gridLineWidth = 0;

            if (!xEnum) {
                options.chart.zoomType = 'xy';
            }
        }

        return options;
    };

    // Simple parser to format data for use by Highcharts.
    var getSeries = function(data) {
        var points = [],
            point,
            datum;

        for (var i = 0; i < data.length; i++) {
            datum = data[i];

            point = _.clone(datum);
            point.x = datum.values[0];

            if (datum.values[1] !== null) {
                point.y = datum.values[1];
            }
            else {
                point.y = datum.count;
            }

            points.push(point);
        }

        return {data: _.sortBy(points, 'x')};
    };

    return {
        processResponse: processResponse,
        getSeries: getSeries
    };

});
<|endoftext|>


<commit_before>var React = require('react');

var Section = require('grommet/components/Section');
var Table = require('grommet/components/Table');
var Form = require('grommet/components/Form');
var FormField = require('grommet/components/FormField');
var CheckBox = require('grommet/components/CheckBox');

var SG_PROPERTIES = {
  type: "number",
  min: "0",
  step: ".001"
};

// nutrient provides ppm per g/L
var nutrients = {
  fermaid_O: {
    organic: 65,
    inorganic: 0
  },
  dap: {
    organic: 0,
    inorganic: 210
  }
};

var YANCalculator = React.createClass({

  getInitialState: function() {
    return {
      final_gravity: 0.997,
      original_gravity: 1.100,
      gravity_units: 'sg',
      volume: 20,
      volume_units: 'liter',
      target_yan: 250,
      // target_yan_units: PPM
      organic_ratio: 30,

      // TODO convert split to generic
      // TODO make split editable...
      steps: [{
        name: 'At Mix',
        when: 0,
        allow_DAP: false,
        dap_split: 0,
        fermaid_O_split: 25
      },{
        name: 'End of Lag',
        when: 0,
        dap_split: 50,
        fermaid_O_split: 25
      },{
        name: '1/6 Sugar Break',
        when: 1/6,
        dap_split: 40,
        fermaid_O_split: 30
      },{
        name: '1/3 Sugar Break',
        when: 1/3,
        dap_split: 10,
        fermaid_O_split: 20
      },{
        total: true,
        name: 'TOTAL',
        dap_split: null,
        fermaid_O_split: null
      }]
      //TODO other properties to do later
      // types of nutrients
      // organic/inorganic ration
      // how many additions

    };
  },

  _onFormSubmit: function(event) {
    event.preventDefault();
  },

  _onChangeCheckBox:function(event) {
    var change = {};
    change[event.target.id] = event.target.checked;
    this.setState(change);
  },

  _onChange: function(event) {
    var change = {};
    change[event.target.id] = event.target.value;
    this.setState(change);
  },

  renderTable: function() {
    var headers = [Nutrient	];
    var total = {
      fermaid_O_split : 0,
      dap_split : 0
    };
    for (var i = this.state.steps.length - 1; i >= 0; i--) {
      total.fermaid_O_split+=this.state.steps[i].fermaid_O_split;
      total.dap_split+=this.state.steps[i].dap_split;
    };
    var total_gravity_points = this.state.original_gravity - this.state.final_gravity;
    var body = {
      gravity: [At Gravity (sg)	],
      fermaid_O_split: [Fermaid O (%)	],
      fermaid_O: [Fermaid O (g)	],
      fermaid_O_YAN: [Fermaid O YAN (ppm)	],
      blank_row: [	],
      dap_split: [DAP (%)	],
      dap: [DAP (g)	],
      dap_YAN: [DAP YAN (ppm)	]
    };
    var simple=['gravity', 'fermaid_O', 'dap'];
    var total_yan = {
      dap: this.state.target_yan * ((100 - this.state.organic_ratio)/100),
      fermaid_O: this.state.target_yan * (this.state.organic_ratio/100)
    };

    for (var i = 0; i < this.state.steps.length; i++) {
      var step = this.state.steps[i];
      headers.push({step.name}	);

      //percentages
      if (!step.total) {
        //TODO do some abstractions
        body.fermaid_O_split.push({(step.fermaid_O_split/total.fermaid_O_split*100).toFixed(2)+'%'}	);
        body.dap_split.push({(step.dap_split/total.dap_split*100).toFixed(2)+'%'}	);

        var dap_YAN = total_yan.dap*step.dap_split/100;
        var fermaid_YAN = total_yan.fermaid_O*step.fermaid_O_split/100;

        body.dap_YAN.push({(dap_YAN).toFixed(2)}	);
        body.dap.push({(dap_YAN*this.state.volume/nutrients.dap.inorganic).toFixed(2)}	);

        body.fermaid_O_YAN.push({(fermaid_YAN).toFixed(2)}	);
        body.fermaid_O.push({(fermaid_YAN*this.state.volume/nutrients.fermaid_O.organic).toFixed(2)}	);

        //TODO round properly
        body.gravity.push({(this.state.original_gravity - (total_gravity_points * step.when)).toFixed(3)}	);
      } else {
        body.fermaid_O_split.push({'100%'}	);
        body.dap_split.push({'100%'}	);

        body.dap_YAN.push({total_yan.dap}	);
        body.dap.push({(total_yan.dap*this.state.volume/nutrients.dap.inorganic).toFixed(2)}	);

        body.fermaid_O_YAN.push({total_yan.fermaid_O}	);
        body.fermaid_O.push({(total_yan.fermaid_O*this.state.volume/nutrients.fermaid_O.organic).toFixed(2)}	);

        body.gravity.push(N/A	);
      }

      // gravities

    };
    var renderedBody =[];
    for (var part in body) {
      if (simple.indexOf(part) !== -1 || this.state.details) {
        renderedBody.push(
{body[part]}
);
      }
    }
    return (

            {headers}

        {renderedBody}

    );
  },

  renderForm: function() {
    return (
      <Form onSubmit={this._onFormSubmit}>
        <FormField label="Volume (liters)" htmlFor="volume" >
          <input id="volume" type="number" onChange={this._onChange} value={this.state.volume}/>
        </FormField>
        <FormField label="Original Gravity (sg)" htmlFor="original_gravity" >
          <input id="original_gravity" {...SG_PROPERTIES} onChange={this._onChange} value={this.state.original_gravity}/>
        </FormField>
        <FormField label="Final Gravity (sg)" htmlFor="final_gravity" >
          <input id="final_gravity" {...SG_PROPERTIES} onChange={this._onChange} value={this.state.final_gravity}/>
        </FormField>
        <FormField label="YAN (ppm)" htmlFor="target_yan" help="225 is a good minimum for low nutrient yeast, 450 a max for high nutrient yeast">
          <input id="target_yan" type="number" onChange={this._onChange} value={this.state.target_yan}/>
        </FormField>
        <FormField label="Organic Percentage (%)" htmlFor="organic_ratio" help="Organic is more expensive, inorganic can cause more yeast blooms">
          <input id="organic_ratio" min="0" max="100" type="number" onChange={this._onChange} value={this.state.organic_ratio}/>
        </FormField>
        <FormField htmlFor="details" help="To see more details in the output table">
          <CheckBox id="details" toggle={true} name="details" label="Show Details" checked={this.state.details} onChange={this._onChangeCheckBox}/>
        </FormField>
      </Form>
    );
  },

  render: function() {

    return (

Here be Inputs

          {this.renderForm()}

Here be Outputs

          {this.renderTable()}

    );
  }
});

module.exports = YANCalculator;
<commit_msg>slightly better numbers.<commit_after>var React = require('react');

var Section = require('grommet/components/Section');
var Table = require('grommet/components/Table');
var Form = require('grommet/components/Form');
var FormField = require('grommet/components/FormField');
var CheckBox = require('grommet/components/CheckBox');

var SG_PROPERTIES = {
  type: "number",
  min: "0",
  step: ".001"
};

// nutrient provides ppm per g/L
var nutrients = {
  fermaid_O: {
    organic: 65,
    inorganic: 0
  },
  dap: {
    organic: 0,
    inorganic: 210
  }
};

var YANCalculator = React.createClass({

  getInitialState: function() {
    return {
      final_gravity: 0.997,
      original_gravity: 1.100,
      gravity_units: 'sg',
      volume: 20,
      volume_units: 'liter',
      target_yan: 250,
      // target_yan_units: PPM
      organic_ratio: 35,

      // TODO convert split to generic
      // TODO make split editable...
      steps: [{
        name: 'At Mix',
        when: 0,
        allow_DAP: false,
        dap_split: 0,
        fermaid_O_split: 40
      },{
        name: 'End of Lag',
        when: 0,
        dap_split: 50,
        fermaid_O_split: 20
      },{
        name: '1/6 Sugar Break',
        when: 1/6,
        dap_split: 40,
        fermaid_O_split: 20
      },{
        name: '1/3 Sugar Break',
        when: 1/3,
        dap_split: 10,
        fermaid_O_split: 20
      },{
        total: true,
        name: 'TOTAL',
        dap_split: null,
        fermaid_O_split: null
      }]
      //TODO other properties to do later
      // types of nutrients
      // organic/inorganic ration
      // how many additions

    };
  },

  _onFormSubmit: function(event) {
    event.preventDefault();
  },

  _onChangeCheckBox:function(event) {
    var change = {};
    change[event.target.id] = event.target.checked;
    this.setState(change);
  },

  _onChange: function(event) {
    var change = {};
    change[event.target.id] = event.target.value;
    this.setState(change);
  },

  renderTable: function() {
    var headers = [Nutrient	];
    var total = {
      fermaid_O_split : 0,
      dap_split : 0
    };
    for (var i = this.state.steps.length - 1; i >= 0; i--) {
      total.fermaid_O_split+=this.state.steps[i].fermaid_O_split;
      total.dap_split+=this.state.steps[i].dap_split;
    };
    var total_gravity_points = this.state.original_gravity - this.state.final_gravity;
    var body = {
      gravity: [At Gravity (sg)	],
      fermaid_O_split: [Fermaid O (%)	],
      fermaid_O: [Fermaid O (g)	],
      fermaid_O_YAN: [Fermaid O YAN (ppm)	],
      blank_row: [	],
      dap_split: [DAP (%)	],
      dap: [DAP (g)	],
      dap_YAN: [DAP YAN (ppm)	]
    };
    var simple=['gravity', 'fermaid_O', 'dap'];
    var total_yan = {
      dap: this.state.target_yan * ((100 - this.state.organic_ratio)/100),
      fermaid_O: this.state.target_yan * (this.state.organic_ratio/100)
    };

    for (var i = 0; i < this.state.steps.length; i++) {
      var step = this.state.steps[i];
      headers.push({step.name}	);

      //percentages
      if (!step.total) {
        //TODO do some abstractions
        body.fermaid_O_split.push({(step.fermaid_O_split/total.fermaid_O_split*100).toFixed(2)+'%'}	);
        body.dap_split.push({(step.dap_split/total.dap_split*100).toFixed(2)+'%'}	);

        var dap_YAN = total_yan.dap*step.dap_split/100;
        var fermaid_YAN = total_yan.fermaid_O*step.fermaid_O_split/100;

        body.dap_YAN.push({(dap_YAN).toFixed(2)}	);
        body.dap.push({(dap_YAN*this.state.volume/nutrients.dap.inorganic).toFixed(2)}	);

        body.fermaid_O_YAN.push({(fermaid_YAN).toFixed(2)}	);
        body.fermaid_O.push({(fermaid_YAN*this.state.volume/nutrients.fermaid_O.organic).toFixed(2)}	);

        //TODO round properly
        body.gravity.push({(this.state.original_gravity - (total_gravity_points * step.when)).toFixed(3)}	);
      } else {
        body.fermaid_O_split.push({'100%'}	);
        body.dap_split.push({'100%'}	);

        body.dap_YAN.push({total_yan.dap}	);
        body.dap.push({(total_yan.dap*this.state.volume/nutrients.dap.inorganic).toFixed(2)}	);

        body.fermaid_O_YAN.push({total_yan.fermaid_O}	);
        body.fermaid_O.push({(total_yan.fermaid_O*this.state.volume/nutrients.fermaid_O.organic).toFixed(2)}	);

        body.gravity.push(N/A	);
      }

      // gravities

    };
    var renderedBody =[];
    for (var part in body) {
      if (simple.indexOf(part) !== -1 || this.state.details) {
        renderedBody.push(
{body[part]}
);
      }
    }
    return (

            {headers}

        {renderedBody}

    );
  },

  renderForm: function() {
    return (
      <Form onSubmit={this._onFormSubmit}>
        <FormField label="Volume (liters)" htmlFor="volume" >
          <input id="volume" type="number" onChange={this._onChange} value={this.state.volume}/>
        </FormField>
        <FormField label="Original Gravity (sg)" htmlFor="original_gravity" >
          <input id="original_gravity" {...SG_PROPERTIES} onChange={this._onChange} value={this.state.original_gravity}/>
        </FormField>
        <FormField label="Final Gravity (sg)" htmlFor="final_gravity" >
          <input id="final_gravity" {...SG_PROPERTIES} onChange={this._onChange} value={this.state.final_gravity}/>
        </FormField>
        <FormField label="YAN (ppm)" htmlFor="target_yan" help="225 is a good minimum for low nutrient yeast, 450 a max for high nutrient yeast">
          <input id="target_yan" type="number" onChange={this._onChange} value={this.state.target_yan}/>
        </FormField>
        <FormField label="Organic Percentage (%)" htmlFor="organic_ratio" help="Organic is more expensive, inorganic can cause more yeast blooms">
          <input id="organic_ratio" min="0" max="100" type="number" onChange={this._onChange} value={this.state.organic_ratio}/>
        </FormField>
        <FormField htmlFor="details" help="To see more details in the output table">
          <CheckBox id="details" toggle={true} name="details" label="Show Details" checked={this.state.details} onChange={this._onChangeCheckBox}/>
        </FormField>
      </Form>
    );
  },

  render: function() {

    return (

Here be Inputs

          {this.renderForm()}

Here be Outputs

          {this.renderTable()}

    );
  }
});

module.exports = YANCalculator;
<|endoftext|>


<commit_before>/**
 * Copyright (c) 2013-2015 Memba Sarl. All rights reserved.
 * Sources at https://github.com/Memba
 */

/* jshint browser: true, jquery: true */
/* globals define: false */

(function (f, define){
    'use strict';
    define([
        './vendor/kendo/kendo.binder',
        './vendor/kendo/kendo.dropdownlist',
        './vendor/kendo/kendo.multiselect' // required becasue of test in kendo.binder.js
    ], f);
})(function () {

    'use strict';

    (function ($, undefined) {

        var kendo = window.kendo,
            Widget = kendo.ui.Widget,
            STRING = 'string',
            CHANGE = 'change',
            NS = '.kendoCodeInput',
            WIDGET_CLASS = 'k-widget kj-codeinput';

        /*********************************************************************************
         * Helpers
         *********************************************************************************/

        function log(message) {
            if (window.app && window.app.DEBUG && window.console && $.isFunction(window.console.log)) {
                window.console.log('kidoju.widgets.codeinput: ' + message);
            }
        }

        /*********************************************************************************
         * Widget
         *********************************************************************************/

        /**
         * @class CodeInput Widget (kendoCodeInput)
         */
        var CodeInput = Widget.extend({

            /**
             * Initializes the widget
             * @param element
             * @param options
             */
            init: function (element, options) {
                var that = this;
                options = options || {};
                Widget.fn.init.call(that, element, options);
                log('widget initialized');
                that._layout();
                that._dataSource();
            },

            /**
             * Widget options
             * @property options
             */
            options: {
                name: 'CodeInput',
                autoBind: true,
                //dataSource
                custom: 'custom',
                default: 'Equal',
                solution: ''
            },

            /**
             * Widget events
             * @property events
             */
            events: [
                CHANGE
            ],

            /**
             * Value for MVVM binding
             * Returns either a JS function as a string or a library formula name
             * @param value
             */
            value: function(value) {
                var that = this;
                if ($.type(value) === STRING) {
                    that._value = value;
                    that._toggle(that._value);
                } else if ($.type(value) === 'undefined') {
                    return that._value;
                } else {
                    throw new TypeError('`value` is expected to be a string if not undefined');
                }
            },

            /**
             * Builds the widget layout
             * @private
             */
            _layout: function () {
                var that = this;
                that.wrapper = that.element;
                that.input = $('<input class="k-textbox k-state-disabled" disabled>')
                    .val(that.options.custom)
                    .appendTo(that.element);
                that.dropDownList = $('<input>')
                    .appendTo(that.element)
                    .kendoDropDownList({
                        autoBind: that.options.autoBind,
                        change: $.proxy(that._onDropDownListChange, that), // change is not triggered by dropDownList api calls incl. value(), text(), ...
                        dataTextField: "name",
                        dataValueField: "formula",
                        dataSource: that.options.dataSource
                    })
                    .data('kendoDropDownList');
            },

            /**
             * Event handler triggered when changing the value of the drop down list in the header
             * @private
             */
            _onDropDownListChange: function() {
                if (this.dropDownList instanceof kendo.ui.DropDownList && this.input instanceof $) {
                    this._value = '// ' + this.dropDownList.text();
                    this.trigger(CHANGE);
                }
            },

            /**
             * Toggle UI for custom vs library code
             * @private
             */
            _toggle: function(value) {
                var that = this;
                if ($.type(value) === STRING && that.dropDownList instanceof kendo.ui.DropDownList && that.input instanceof $) {
                    var libraryMatches = value.match(/^\/\/ ([^\n]+)$/),
                        customMatches = value.match(/^function validate\(value, solution\) {[\s\S]+}$/);
                    if ($.isArray(libraryMatches) && libraryMatches.length === 2) {
                        // Find in the code library
                        var found = that.dropDownList.dataSource.data().filter(function(item) {
                            return item.name === libraryMatches[1];
                        });
                        found = $.isArray(found) && found.length ? libraryMatches[1] : that.options.default;
                        that.dropDownList.text(found);
                        that.dropDownList.wrapper.show();
                        that.input.hide();
                    } else if ($.isArray(customMatches) && customMatches.length === 1) {
                        that.dropDownList.wrapper.hide();
                        that.input.show();
                    } else {
                        that.dropDownList.text(that.options.default);
                        that.dropDownList.wrapper.show();
                        that.input.hide();
                    }
                }
            },

            /**
             * _dataSource function to bind refresh to the change event
             * @private
             */
            _dataSource: function() {
                var that = this,
                    dropDownList = that.dropDownList;
                if (dropDownList instanceof kendo.ui.DropDownList && dropDownList.dataSource instanceof kendo.data.DataSource) {
                    // MVVM bindings require that.dataSource
                    that.dataSource = that.dropDownList.dataSource;
                    if (that._refreshHandler) {
                        that.dataSource.unbind(CHANGE, that._refreshHandler);
                    }
                    that._refreshHandler = $.proxy(that.refresh, that);
                    that.dataSource.bind(CHANGE, that._refreshHandler);
                }
            },

            /**
             * sets the dataSource for source binding
             * @param dataSource
             */
            setDataSource: function(dataSource) {
                var that = this,
                    dropDownList = that.dropDownList;
                if (dropDownList instanceof kendo.ui.DropDownList && dropDownList.dataSource !== dataSource ) {
                    dropDownList.setDataSource(dataSource);
                    that._dataSource();
                }
            },

            /**
             * Refresh
             * @param e
             */
            refresh: function(e) {
                this.dropDownList.refresh(e);
            },

            /**
             * Clears the widget
             * @method _clear
             * @private
             */
            _clear: function () {
                var that = this;
                // unbind kendo
                // kendo.unbind($(that.element));
                // unbind all other events
                $(that.element).find('*').off();
                $(that.element).off();
                // remove descendants
                $(that.element).empty();
                // remove element classes
                // $(that.element).removeClass(WIDGET_CLASS);
            },

            /**
             * Destroys the widget including all DOM modifications
             * @method destroy
             */
            destroy: function () {
                var that = this;
                Widget.fn.destroy.call(that);
                that._clear();
                kendo.destroy(that.element);
            }

        });

        kendo.ui.plugin(CodeInput);

    }(window.jQuery));

    return window.kendo;

}, typeof define === 'function' && define.amd ? define : function (_, f){ 'use strict'; f(); });
<commit_msg>Added width 100%<commit_after>/**
 * Copyright (c) 2013-2015 Memba Sarl. All rights reserved.
 * Sources at https://github.com/Memba
 */

/* jshint browser: true, jquery: true */
/* globals define: false */

(function (f, define){
    'use strict';
    define([
        './vendor/kendo/kendo.binder',
        './vendor/kendo/kendo.dropdownlist',
        './vendor/kendo/kendo.multiselect' // required becasue of test in kendo.binder.js
    ], f);
})(function () {

    'use strict';

    (function ($, undefined) {

        var kendo = window.kendo,
            Widget = kendo.ui.Widget,
            STRING = 'string',
            CHANGE = 'change',
            NS = '.kendoCodeInput',
            WIDGET_CLASS = 'k-widget kj-codeinput';

        /*********************************************************************************
         * Helpers
         *********************************************************************************/

        function log(message) {
            if (window.app && window.app.DEBUG && window.console && $.isFunction(window.console.log)) {
                window.console.log('kidoju.widgets.codeinput: ' + message);
            }
        }

        /*********************************************************************************
         * Widget
         *********************************************************************************/

        /**
         * @class CodeInput Widget (kendoCodeInput)
         */
        var CodeInput = Widget.extend({

            /**
             * Initializes the widget
             * @param element
             * @param options
             */
            init: function (element, options) {
                var that = this;
                options = options || {};
                Widget.fn.init.call(that, element, options);
                log('widget initialized');
                that._layout();
                that._dataSource();
            },

            /**
             * Widget options
             * @property options
             */
            options: {
                name: 'CodeInput',
                autoBind: true,
                //dataSource
                custom: 'custom',
                default: 'Equal',
                solution: ''
            },

            /**
             * Widget events
             * @property events
             */
            events: [
                CHANGE
            ],

            /**
             * Value for MVVM binding
             * Returns either a JS function as a string or a library formula name
             * @param value
             */
            value: function(value) {
                var that = this;
                if ($.type(value) === STRING) {
                    that._value = value;
                    that._toggle(that._value);
                } else if ($.type(value) === 'undefined') {
                    return that._value;
                } else {
                    throw new TypeError('`value` is expected to be a string if not undefined');
                }
            },

            /**
             * Builds the widget layout
             * @private
             */
            _layout: function () {
                var that = this;
                that.wrapper = that.element;
                that.input = $('<input class="k-textbox k-state-disabled" disabled>')
                    .width('100%')
                    .val(that.options.custom)
                    .appendTo(that.element);
                that.dropDownList = $('<input>')
                    .width('100%')
                    .appendTo(that.element)
                    .kendoDropDownList({
                        autoBind: that.options.autoBind,
                        change: $.proxy(that._onDropDownListChange, that), // change is not triggered by dropDownList api calls incl. value(), text(), ...
                        dataTextField: "name",
                        dataValueField: "formula",
                        dataSource: that.options.dataSource
                    })
                    .data('kendoDropDownList');
            },

            /**
             * Event handler triggered when changing the value of the drop down list in the header
             * @private
             */
            _onDropDownListChange: function() {
                if (this.dropDownList instanceof kendo.ui.DropDownList && this.input instanceof $) {
                    this._value = '// ' + this.dropDownList.text();
                    this.trigger(CHANGE);
                }
            },

            /**
             * Toggle UI for custom vs library code
             * @private
             */
            _toggle: function(value) {
                var that = this;
                if ($.type(value) === STRING && that.dropDownList instanceof kendo.ui.DropDownList && that.input instanceof $) {
                    var libraryMatches = value.match(/^\/\/ ([^\n]+)$/),
                        customMatches = value.match(/^function validate\(value, solution\) {[\s\S]+}$/);
                    if ($.isArray(libraryMatches) && libraryMatches.length === 2) {
                        // Find in the code library
                        var found = that.dropDownList.dataSource.data().filter(function(item) {
                            return item.name === libraryMatches[1];
                        });
                        found = $.isArray(found) && found.length ? libraryMatches[1] : that.options.default;
                        that.dropDownList.text(found);
                        that.dropDownList.wrapper.show();
                        that.input.hide();
                    } else if ($.isArray(customMatches) && customMatches.length === 1) {
                        that.dropDownList.wrapper.hide();
                        that.input.show();
                    } else {
                        that.dropDownList.text(that.options.default);
                        that.dropDownList.wrapper.show();
                        that.input.hide();
                    }
                }
            },

            /**
             * _dataSource function to bind refresh to the change event
             * @private
             */
            _dataSource: function() {
                var that = this,
                    dropDownList = that.dropDownList;
                if (dropDownList instanceof kendo.ui.DropDownList && dropDownList.dataSource instanceof kendo.data.DataSource) {
                    // MVVM bindings require that.dataSource
                    that.dataSource = that.dropDownList.dataSource;
                    if (that._refreshHandler) {
                        that.dataSource.unbind(CHANGE, that._refreshHandler);
                    }
                    that._refreshHandler = $.proxy(that.refresh, that);
                    that.dataSource.bind(CHANGE, that._refreshHandler);
                }
            },

            /**
             * sets the dataSource for source binding
             * @param dataSource
             */
            setDataSource: function(dataSource) {
                var that = this,
                    dropDownList = that.dropDownList;
                if (dropDownList instanceof kendo.ui.DropDownList && dropDownList.dataSource !== dataSource ) {
                    dropDownList.setDataSource(dataSource);
                    that._dataSource();
                }
            },

            /**
             * Refresh
             * @param e
             */
            refresh: function(e) {
                this.dropDownList.refresh(e);
            },

            /**
             * Clears the widget
             * @method _clear
             * @private
             */
            _clear: function () {
                var that = this;
                // unbind kendo
                // kendo.unbind($(that.element));
                // unbind all other events
                $(that.element).find('*').off();
                $(that.element).off();
                // remove descendants
                $(that.element).empty();
                // remove element classes
                // $(that.element).removeClass(WIDGET_CLASS);
            },

            /**
             * Destroys the widget including all DOM modifications
             * @method destroy
             */
            destroy: function () {
                var that = this;
                Widget.fn.destroy.call(that);
                that._clear();
                kendo.destroy(that.element);
            }

        });

        kendo.ui.plugin(CodeInput);

    }(window.jQuery));

    return window.kendo;

}, typeof define === 'function' && define.amd ? define : function (_, f){ 'use strict'; f(); });
<|endoftext|>


<commit_before>import classNames from 'classnames';
import {Confirm} from 'reactjs-components';
import mixin from 'reactjs-mixin';
import prettycron from 'prettycron';
/* eslint-disable no-unused-vars */
import React from 'react';
/* eslint-enable no-unused-vars */
import {routerShape} from 'react-router';

import {StoreMixin} from 'mesosphere-shared-reactjs';

import Icon from '../../components/Icon';
import JobConfiguration from './JobConfiguration';
import JobFormModal from '../../components/modals/JobFormModal';
import JobRunHistoryTable from './JobRunHistoryTable';
import JobsBreadcrumbs from '../../components/breadcrumbs/JobsBreadcrumbs';
import Loader from '../../components/Loader';
import MetronomeStore from '../../stores/MetronomeStore';
import Page from '../../components/Page';
import RequestErrorMsg from '../../components/RequestErrorMsg';
import StringUtil from '../../utils/StringUtil';
import TabsMixin from '../../mixins/TabsMixin';
import TimeAgo from '../../components/TimeAgo';
import TaskStates from '../../../../plugins/services/src/js/constants/TaskStates';

const METHODS_TO_BIND = [
  'closeDialog',
  'handleEditButtonClick',
  'handleRunNowButtonClick',
  'handleDisableScheduleButtonClick',
  'handleEnableScheduleButtonClick',
  'handleDestroyButtonClick',
  'onMetronomeStoreJobDeleteError',
  'onMetronomeStoreJobDeleteSuccess',
  'onMetronomeStoreJobDetailError',
  'onMetronomeStoreJobDetailChange'
];

const DIALOGS = {
  EDIT: 'edit',
  DESTROY: 'destroy'
};

class JobDetailPage extends mixin(StoreMixin, TabsMixin) {
  constructor() {
    super(...arguments);

    this.store_listeners = [{
      name: 'metronome',
      events: [
        'jobDeleteSuccess',
        'jobDeleteError',
        'jobDetailChange',
        'jobDetailError',
        'jobRunError',
        'jobRunSuccess',
        'jobScheduleUpdateError',
        'jobScheduleUpdateSuccess'
      ],
      suppressUpdate: false
    }];

    this.tabs_tabs = {
      runHistory: 'Run History',
      configuration: 'Configuration'
    };

    this.state = {
      currentTab: Object.keys(this.tabs_tabs).shift(),
      disabledDialog: null,
      jobActionDialog: null,
      errorMsg: null,
      errorCount: 0,
      isJobFormModalOpen: false,
      isLoading: true
    };

    METHODS_TO_BIND.forEach((method) => {
      this[method] = this[method].bind(this);
    });
  }

  componentDidMount() {
    super.componentDidMount(...arguments);
    MetronomeStore.monitorJobDetail(this.props.params.id);
  }

  componentWillUnmount() {
    super.componentWillUnmount(...arguments);
    MetronomeStore.stopJobDetailMonitor(this.props.params.id);
  }

  onMetronomeStoreJobDeleteError(id, {message:errorMsg}) {
    if (id !== this.props.params.id || errorMsg == null) {
      return;
    }

    this.setState({
      jobActionDialog: DIALOGS.DESTROY,
      disabledDialog: null,
      errorMsg
    });
  }

  onMetronomeStoreJobDeleteSuccess() {
    this.closeDialog();
    this.context.router.push('/jobs');
  }

  onMetronomeStoreJobDetailError() {
    this.setState({errorCount: this.state.errorCount + 1});
  }

  onMetronomeStoreJobDetailChange() {
    this.setState({errorCount: 0, isLoading: false});
  }

  handleEditButtonClick() {
    this.setState({jobActionDialog: DIALOGS.EDIT});
  }

  handleRunNowButtonClick() {
    const job = MetronomeStore.getJob(this.props.params.id);

    MetronomeStore.runJob(job.getId());
  }

  handleDisableScheduleButtonClick() {
    MetronomeStore.toggleSchedule(this.props.params.id, false);
  };

  handleEnableScheduleButtonClick() {
    MetronomeStore.toggleSchedule(this.props.params.id, true);
  };

  handleDestroyButtonClick() {
    this.setState({jobActionDialog: DIALOGS.DESTROY});
  };

  handleAcceptDestroyDialog(stopCurrentJobRuns = false) {
    this.setState({disabledDialog: DIALOGS.DESTROY}, () => {
      MetronomeStore.deleteJob(this.props.params.id, stopCurrentJobRuns);
    });
  }

  handleMoreDropdownSelection(selection) {
    if (selection.id === DIALOGS.SCHEDULE_DISABLE) {
      MetronomeStore.toggleSchedule(this.props.params.id, false);

      return;
    }

    if (selection.id === DIALOGS.SCHEDULE_ENABLE) {
      MetronomeStore.toggleSchedule(this.props.params.id, true);

      return;
    }

    this.setState({jobActionDialog: selection.id});
  }

  closeDialog() {
    this.setState({
      disabledDialog: null,
      errorMsg: null,
      jobActionDialog: null
    });
  }

  getDestroyConfirmDialog() {
    const {id} = this.props.params;
    const {disabledDialog, jobActionDialog, errorMsg} = this.state;
    let stopCurrentJobRuns = false;
    let actionButtonLabel = 'Destroy Job';
    let message = `Are you sure you want to destroy ${id}? ` +
      'This action is irreversible.';

    if (/stopCurrentJobRuns=true/.test(errorMsg)) {
      actionButtonLabel = 'Stop Current Runs and Destroy Job';
      stopCurrentJobRuns = true;
      message = `Couldn't destroy ${id} as there are currently active job ` +
        'runs. Do you want to stop all runs and destroy the job?';
    }

    let content = (

          Destroy Job

{message}

    );

    return (
      <Confirm children={content}
        disabled={disabledDialog === DIALOGS.DESTROY}
        open={jobActionDialog === DIALOGS.DESTROY}
        onClose={this.closeDialog}
        leftButtonText="Cancel"
        leftButtonCallback={this.closeDialog}
        rightButtonText={actionButtonLabel}
        rightButtonClassName="button button-danger"
        rightButtonCallback=
          {this.handleAcceptDestroyDialog.bind(this, stopCurrentJobRuns)} />
    );
  }

  getErrorScreen() {
    return (

        <RequestErrorMsg />

    );
  }

  getLoadingScreen() {
    return (
      <Page>
        <Page.Header breadcrumbs={<JobsBreadcrumbs/>} />
        <Loader />
      </Page>
    );
  }

  getNavigationTabs() {
    return (

        {this.tabs_getUnroutedTabs()}

    );
  }

  getPrettySchedule(job) {
    const schedules = job.getSchedules();
    if (schedules == null || schedules.length === 0) {
      return null;
    }

    const schedule = schedules[0];
    if (schedule.enabled) {
      return prettycron.toString(schedule.cron);
    }
  }

  getJobStatus(job) {
    let longestRunningTask = null;
    const longestRunningActiveRun = job.getActiveRuns()
      .getLongestRunningActiveRun();

    if (longestRunningActiveRun != null) {
      longestRunningTask = longestRunningActiveRun.getTasks()
        .getLongestRunningTask();
    }

    if (longestRunningTask == null) {
      return null;
    }

    const status = TaskStates[longestRunningTask.getStatus()];

    return status.displayName;
  }

  getSubTitle(job) {
    const nodes = [];
    const scheduleText = this.getPrettySchedule(job);
    let longestRunningTask = null;
    const longestRunningActiveRun = job.getActiveRuns()
      .getLongestRunningActiveRun();

    if (longestRunningActiveRun != null) {
      longestRunningTask = longestRunningActiveRun.getTasks()
        .getLongestRunningTask();
    }

    if (longestRunningTask == null && scheduleText == null) {
      return null;
    }

    if (scheduleText != null) {
      nodes.push(

          <Icon
            className="icon-margin-right"
            key="schedule-icon"
            color="grey"
            id="repeat"
            size="mini" />
          Scheduled {StringUtil.lowercase(scheduleText)}

      );
    }

    if (longestRunningTask != null) {
      const dateCompleted = longestRunningTask.getDateCompleted();

      const status = TaskStates[longestRunningTask.getStatus()];
      let statusClasses = classNames('job-details-header-status', {
        'text-success': status.stateTypes.includes('success')
        && !status.stateTypes.includes('failure'),
        'text-danger': status.stateTypes.includes('failure')
      });

      let timePrefix = null;
      let shouldSuppressRelativeTime = dateCompleted == null;

      if (shouldSuppressRelativeTime) {
        timePrefix = 'for ';
      }

      nodes.push(

          {status.displayName}
        ,

          <TimeAgo
            prefix={timePrefix}
            suppressSuffix={shouldSuppressRelativeTime}
            time={longestRunningActiveRun.getDateCreated()} />

      );
    }

    return nodes;
  }

  renderConfigurationTabView(job) {
    return <JobConfiguration job={job} />;
  }

  renderRunHistoryTabView(job) {
    return <JobRunHistoryTable job={job} />;
  }

  getActions() {
    const job = MetronomeStore.getJob(this.props.params.id);
    const [schedule] = job.getSchedules();

    const actions = [];

    actions.push({
      label: 'Edit',
      onItemSelect: this.handleEditButtonClick
    });

    actions.push({
      label: 'Run Now',
      onItemSelect: this.handleRunNowButtonClick
    });

    if (schedule != null && schedule.enabled) {
      actions.push({
        label: 'Disable Schedule',
        onItemSelect: this.handleDisableScheduleButtonClick
      });
    }

    if (schedule != null && !schedule.enabled) {
      actions.push({
        label: 'Enable Schedule',
        onItemSelect: this.handleEnableScheduleButtonClick
      });
    }

    actions.push({
      className: 'text-danger',
      label: 'Destroy',
      onItemSelect: this.handleDestroyButtonClick
    });

    return actions;
  }

  getTabs() {
    const activeTab = this.state.currentTab;

    return [
      {
        label: 'Run History',
        callback: () => { this.setState({currentTab: 'runHistory'}); },
        isActive: activeTab === 'runHistory'
      },
      {
        label: 'Configuration',
        callback: () => { this.setState({currentTab: 'configuration'}); },
        isActive: activeTab === 'configuration'
      }
    ];
  }

  render() {
    if (this.state.errorCount > 3) {
      return this.getErrorScreen();
    }

    if (this.state.isLoading) {
      return this.getLoadingScreen();
    }

    // TaskDetailView
    if (this.props.params.taskID) {
      return this.props.children;
    }

    let job = MetronomeStore.getJob(this.props.params.id);
    const jobStatus = this.getJobStatus(job);

    return (
      <Page>
        <Page.Header
          actions={this.getActions()}
          breadcrumbs={<JobsBreadcrumbs jobID={job.getId()} jobStatus={jobStatus} />}
          tabs={this.getTabs()} />
        {this.tabs_getTabView(job)}
        <JobFormModal isEdit={true}
          job={job}
          open={this.state.jobActionDialog === DIALOGS.EDIT}
          onClose={this.closeDialog} />
        {this.getDestroyConfirmDialog()}
      </Page>
    );
  }
}

JobDetailPage.contextTypes = {
  router: routerShape
};

module.exports = JobDetailPage;
<commit_msg>Wrapping jobs error page in a <Page/><commit_after>import classNames from 'classnames';
import {Confirm} from 'reactjs-components';
import mixin from 'reactjs-mixin';
import prettycron from 'prettycron';
/* eslint-disable no-unused-vars */
import React from 'react';
/* eslint-enable no-unused-vars */
import {routerShape} from 'react-router';

import {StoreMixin} from 'mesosphere-shared-reactjs';

import Icon from '../../components/Icon';
import JobConfiguration from './JobConfiguration';
import JobFormModal from '../../components/modals/JobFormModal';
import JobRunHistoryTable from './JobRunHistoryTable';
import JobsBreadcrumbs from '../../components/breadcrumbs/JobsBreadcrumbs';
import Loader from '../../components/Loader';
import MetronomeStore from '../../stores/MetronomeStore';
import Page from '../../components/Page';
import RequestErrorMsg from '../../components/RequestErrorMsg';
import StringUtil from '../../utils/StringUtil';
import TabsMixin from '../../mixins/TabsMixin';
import TimeAgo from '../../components/TimeAgo';
import TaskStates from '../../../../plugins/services/src/js/constants/TaskStates';

const METHODS_TO_BIND = [
  'closeDialog',
  'handleEditButtonClick',
  'handleRunNowButtonClick',
  'handleDisableScheduleButtonClick',
  'handleEnableScheduleButtonClick',
  'handleDestroyButtonClick',
  'onMetronomeStoreJobDeleteError',
  'onMetronomeStoreJobDeleteSuccess',
  'onMetronomeStoreJobDetailError',
  'onMetronomeStoreJobDetailChange'
];

const DIALOGS = {
  EDIT: 'edit',
  DESTROY: 'destroy'
};

class JobDetailPage extends mixin(StoreMixin, TabsMixin) {
  constructor() {
    super(...arguments);

    this.store_listeners = [{
      name: 'metronome',
      events: [
        'jobDeleteSuccess',
        'jobDeleteError',
        'jobDetailChange',
        'jobDetailError',
        'jobRunError',
        'jobRunSuccess',
        'jobScheduleUpdateError',
        'jobScheduleUpdateSuccess'
      ],
      suppressUpdate: false
    }];

    this.tabs_tabs = {
      runHistory: 'Run History',
      configuration: 'Configuration'
    };

    this.state = {
      currentTab: Object.keys(this.tabs_tabs).shift(),
      disabledDialog: null,
      jobActionDialog: null,
      errorMsg: null,
      errorCount: 0,
      isJobFormModalOpen: false,
      isLoading: true
    };

    METHODS_TO_BIND.forEach((method) => {
      this[method] = this[method].bind(this);
    });
  }

  componentDidMount() {
    super.componentDidMount(...arguments);
    MetronomeStore.monitorJobDetail(this.props.params.id);
  }

  componentWillUnmount() {
    super.componentWillUnmount(...arguments);
    MetronomeStore.stopJobDetailMonitor(this.props.params.id);
  }

  onMetronomeStoreJobDeleteError(id, {message:errorMsg}) {
    if (id !== this.props.params.id || errorMsg == null) {
      return;
    }

    this.setState({
      jobActionDialog: DIALOGS.DESTROY,
      disabledDialog: null,
      errorMsg
    });
  }

  onMetronomeStoreJobDeleteSuccess() {
    this.closeDialog();
    this.context.router.push('/jobs');
  }

  onMetronomeStoreJobDetailError() {
    this.setState({errorCount: this.state.errorCount + 1});
  }

  onMetronomeStoreJobDetailChange() {
    this.setState({errorCount: 0, isLoading: false});
  }

  handleEditButtonClick() {
    this.setState({jobActionDialog: DIALOGS.EDIT});
  }

  handleRunNowButtonClick() {
    const job = MetronomeStore.getJob(this.props.params.id);

    MetronomeStore.runJob(job.getId());
  }

  handleDisableScheduleButtonClick() {
    MetronomeStore.toggleSchedule(this.props.params.id, false);
  };

  handleEnableScheduleButtonClick() {
    MetronomeStore.toggleSchedule(this.props.params.id, true);
  };

  handleDestroyButtonClick() {
    this.setState({jobActionDialog: DIALOGS.DESTROY});
  };

  handleAcceptDestroyDialog(stopCurrentJobRuns = false) {
    this.setState({disabledDialog: DIALOGS.DESTROY}, () => {
      MetronomeStore.deleteJob(this.props.params.id, stopCurrentJobRuns);
    });
  }

  handleMoreDropdownSelection(selection) {
    if (selection.id === DIALOGS.SCHEDULE_DISABLE) {
      MetronomeStore.toggleSchedule(this.props.params.id, false);

      return;
    }

    if (selection.id === DIALOGS.SCHEDULE_ENABLE) {
      MetronomeStore.toggleSchedule(this.props.params.id, true);

      return;
    }

    this.setState({jobActionDialog: selection.id});
  }

  closeDialog() {
    this.setState({
      disabledDialog: null,
      errorMsg: null,
      jobActionDialog: null
    });
  }

  getDestroyConfirmDialog() {
    const {id} = this.props.params;
    const {disabledDialog, jobActionDialog, errorMsg} = this.state;
    let stopCurrentJobRuns = false;
    let actionButtonLabel = 'Destroy Job';
    let message = `Are you sure you want to destroy ${id}? ` +
      'This action is irreversible.';

    if (/stopCurrentJobRuns=true/.test(errorMsg)) {
      actionButtonLabel = 'Stop Current Runs and Destroy Job';
      stopCurrentJobRuns = true;
      message = `Couldn't destroy ${id} as there are currently active job ` +
        'runs. Do you want to stop all runs and destroy the job?';
    }

    let content = (

          Destroy Job

{message}

    );

    return (
      <Confirm children={content}
        disabled={disabledDialog === DIALOGS.DESTROY}
        open={jobActionDialog === DIALOGS.DESTROY}
        onClose={this.closeDialog}
        leftButtonText="Cancel"
        leftButtonCallback={this.closeDialog}
        rightButtonText={actionButtonLabel}
        rightButtonClassName="button button-danger"
        rightButtonCallback=
          {this.handleAcceptDestroyDialog.bind(this, stopCurrentJobRuns)} />
    );
  }

  getErrorScreen() {
    return (
      <Page>
        <Page.Header breadcrumbs={<JobsBreadcrumbs/>} />

          <RequestErrorMsg />

      </Page>
    );
  }

  getLoadingScreen() {
    return (
      <Page>
        <Page.Header breadcrumbs={<JobsBreadcrumbs/>} />
        <Loader />
      </Page>
    );
  }

  getNavigationTabs() {
    return (

        {this.tabs_getUnroutedTabs()}

    );
  }

  getPrettySchedule(job) {
    const schedules = job.getSchedules();
    if (schedules == null || schedules.length === 0) {
      return null;
    }

    const schedule = schedules[0];
    if (schedule.enabled) {
      return prettycron.toString(schedule.cron);
    }
  }

  getJobStatus(job) {
    let longestRunningTask = null;
    const longestRunningActiveRun = job.getActiveRuns()
      .getLongestRunningActiveRun();

    if (longestRunningActiveRun != null) {
      longestRunningTask = longestRunningActiveRun.getTasks()
        .getLongestRunningTask();
    }

    if (longestRunningTask == null) {
      return null;
    }

    const status = TaskStates[longestRunningTask.getStatus()];

    return status.displayName;
  }

  getSubTitle(job) {
    const nodes = [];
    const scheduleText = this.getPrettySchedule(job);
    let longestRunningTask = null;
    const longestRunningActiveRun = job.getActiveRuns()
      .getLongestRunningActiveRun();

    if (longestRunningActiveRun != null) {
      longestRunningTask = longestRunningActiveRun.getTasks()
        .getLongestRunningTask();
    }

    if (longestRunningTask == null && scheduleText == null) {
      return null;
    }

    if (scheduleText != null) {
      nodes.push(

          <Icon
            className="icon-margin-right"
            key="schedule-icon"
            color="grey"
            id="repeat"
            size="mini" />
          Scheduled {StringUtil.lowercase(scheduleText)}

      );
    }

    if (longestRunningTask != null) {
      const dateCompleted = longestRunningTask.getDateCompleted();

      const status = TaskStates[longestRunningTask.getStatus()];
      let statusClasses = classNames('job-details-header-status', {
        'text-success': status.stateTypes.includes('success')
        && !status.stateTypes.includes('failure'),
        'text-danger': status.stateTypes.includes('failure')
      });

      let timePrefix = null;
      let shouldSuppressRelativeTime = dateCompleted == null;

      if (shouldSuppressRelativeTime) {
        timePrefix = 'for ';
      }

      nodes.push(

          {status.displayName}
        ,

          <TimeAgo
            prefix={timePrefix}
            suppressSuffix={shouldSuppressRelativeTime}
            time={longestRunningActiveRun.getDateCreated()} />

      );
    }

    return nodes;
  }

  renderConfigurationTabView(job) {
    return <JobConfiguration job={job} />;
  }

  renderRunHistoryTabView(job) {
    return <JobRunHistoryTable job={job} />;
  }

  getActions() {
    const job = MetronomeStore.getJob(this.props.params.id);
    const [schedule] = job.getSchedules();

    const actions = [];

    actions.push({
      label: 'Edit',
      onItemSelect: this.handleEditButtonClick
    });

    actions.push({
      label: 'Run Now',
      onItemSelect: this.handleRunNowButtonClick
    });

    if (schedule != null && schedule.enabled) {
      actions.push({
        label: 'Disable Schedule',
        onItemSelect: this.handleDisableScheduleButtonClick
      });
    }

    if (schedule != null && !schedule.enabled) {
      actions.push({
        label: 'Enable Schedule',
        onItemSelect: this.handleEnableScheduleButtonClick
      });
    }

    actions.push({
      className: 'text-danger',
      label: 'Destroy',
      onItemSelect: this.handleDestroyButtonClick
    });

    return actions;
  }

  getTabs() {
    const activeTab = this.state.currentTab;

    return [
      {
        label: 'Run History',
        callback: () => { this.setState({currentTab: 'runHistory'}); },
        isActive: activeTab === 'runHistory'
      },
      {
        label: 'Configuration',
        callback: () => { this.setState({currentTab: 'configuration'}); },
        isActive: activeTab === 'configuration'
      }
    ];
  }

  render() {
    if (this.state.errorCount > 3) {
      return this.getErrorScreen();
    }

    if (this.state.isLoading) {
      return this.getLoadingScreen();
    }

    // TaskDetailView
    if (this.props.params.taskID) {
      return this.props.children;
    }

    let job = MetronomeStore.getJob(this.props.params.id);
    const jobStatus = this.getJobStatus(job);

    return (
      <Page>
        <Page.Header
          actions={this.getActions()}
          breadcrumbs={<JobsBreadcrumbs jobID={job.getId()} jobStatus={jobStatus} />}
          tabs={this.getTabs()} />
        {this.tabs_getTabView(job)}
        <JobFormModal isEdit={true}
          job={job}
          open={this.state.jobActionDialog === DIALOGS.EDIT}
          onClose={this.closeDialog} />
        {this.getDestroyConfirmDialog()}
      </Page>
    );
  }
}

JobDetailPage.contextTypes = {
  router: routerShape
};

module.exports = JobDetailPage;
<|endoftext|>


<commit_before>/*
MIT License

Copyright (c) 2016 jrvieira

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

'use strict';

//export default chain; //import chain from 'chainchainchain'
module.exports = chain;

//every setting defaults to false
var settings = Object.preventExtensions({
	owncontext: false, //functions remain in owner's context
	//uniqueness: false, //prevent duplicate objects in chain
	allowloops: false, //allow loops (only happens when chaining Ch objects)
	setchained: false //values are set on its youngest owner in chain
});

const chi = Symbol('chi');

var handler = {
	get: function (target, prop, receiver) {
		if (prop === chi) return target[chi];
		//if (prop === rvk) return target.
		//get youngest
		var avoid = chain.findloop(target);
		if (!settings.allowloops && avoid) throw new Error('Loop in chain', avoid);

		for (let o of target[chi]) {

			if (avoid && avoid === o) return false;

			if (typeof o[prop] !== 'undefined') {
				//x && y => if x is true returns y, else returns x
				return o[prop] instanceof Function ? o[prop].bind(!settings.owncontext && receiver || o) : o[prop];
			}
		}
		return undefined;
	},
	set: function (target, prop, value) {
		if (prop === chi) return target[chi] = value;
		var origin = target[chi][0];
		if (!settings.setchained) return origin[prop] = value;
		//set youngest
		for (let o of target[chi]) {
			if (typeof o[prop] !== 'undefined') {
				return o[prop] = value;
			}
		}
		console.warn(prop+' is not a chained property');
		return false;
	}
}

function Ch (arr) {
	this[chi] = arr;
}

function chain (...oo) {
	//validate o
	if (typeof oo === 'undefined') throw new TypeError('Argument undefined');
	let arr = (oo.length) ? oo : [{}];
	//validate ch
	console.log('// ch init');

	var ch = new Proxy(new Ch(arr), handler);
	//find loops
	if (chain.findloop(ch)) {
		if (!settings.allowloops) throw new Error('Loop in chain', chain.findloop(ch));
		console.warn('Loop in chain', chain.findloop(ch));
	}

	return ch;
}

Object.defineProperties(chain, {

	set: {
		value: new Proxy(settings, {
			get: function (target, prop) {
				return !!target[prop];
			},
			set: function (target, prop, value) {
				//if (!(prop in target)) throw new Error(prop+' is not a setting');
				if (typeof value !== 'boolean') console.warn('Chain settings should be Boolean, assuming '+!!value+' for '+prop);
				return target[prop] = !!value;
			}
		})
	},
	//if you create a loop there is a RangeError: Maximum call stack size exceeded
	//so this method and the allowloops setting is obsolete ?
	findloop: {
		value: function (ch, set = new Set()) {
			set.add(ch);
			for (let o of ch[chi]) {
				if ( chain.is(o) && ( set.has(o) || chain.findloop(o, set) ) ) {
					return o;
				}
			}
			return false;
		}
	},
	//returns true if object is chain
	is: {
		value: function (ch) {
			return ch instanceof Ch ? true : false;
		}
	},
	//returns origin object
	origin: {
		value: function (ch) {
			return ch[chi][0] || false;
		}
	},
	//returns chain's array of objects
	arr: {
		value: function (ch) {
			return ch[chi] ? [...ch[chi]] : false;
		}
	},
	//returns array of properties in chain
	raw: {
		value: function (ch, prop, callback) {
			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain');
			//validate p
			if (typeof prop === 'undefined') throw new TypeError('Argument undefined');

			var raw = [];
			var own = [];

			for (let o of ch[chi]) {
				//x && y => if x is true returns y, else returns x
				raw.push(o[prop] instanceof Function ? o[prop].bind(!settings.owncontext && ch[chi][0] || o) : o[prop]);
				if (callback) own.push(o);
			}

			return callback ? callback(raw, own) : raw;
		}
	},
	//MANIPULATION
	//adds objects to end of chain
	add: {
		value: function (ch, oo = []) {
			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain');
			if (!(oo instanceof Array)) oo = [oo];
			//appends objects to chain
			ch[chi].push(...oo);
			//find loops
			if (chain.findloop(ch)) {
				if (!settings.allowloops) throw new Error('Loop in chain', chain.findloop(ch));
				console.warn('Loop in chain', chain.findloop(ch));
			}

			return [...ch[chi]];
		}
	},
	//adds objects to beggining of chain
	pre: {
		value: function (ch, oo = []) {
			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain');
			if (!(oo instanceof Array)) oo = [oo];
			//prepends objects to chain
			ch[chi].unshift(...oo);
			//find loops
			if (chain.findloop(ch)) {
				if (!settings.allowloops) throw new Error('Loop in chain', chain.findloop(ch));
				console.warn('Loop in chain', chain.findloop(ch));
			}

			return [...ch[chi]];
		}
	},
	//replaces objects from chain
	rep: {
		value: function (ch, x, o) {
			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain');
			//replaces objects in chain
			while (!(ch[chi].indexOf(x) < 0)) {
				ch[chi].splice(ch[chi].indexOf(x),1,o);
			}
			//find loops
			if (chain.findloop(ch)) {
				if (!settings.allowloops) throw new Error('Loop in chain', chain.findloop(ch));
				console.warn('Loop in chain', chain.findloop(ch));
			}

			return [...ch[chi]];
		}
	},
	//removes objects from chain
	rem: {
		value: function (ch, xx = []) {
			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain');
			if (!(xx instanceof Array)) xx = [xx];
			//filters objects out of chain
			ch[chi] = ch[chi].filter(function (o) {
				return !xx.includes(o);
			});

			return [...ch[chi]];
		}
	},
	//removes last object from chain
	pop: {
		value: function (ch) {
			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain');

			return ch[chi].pop();
		}
	},
	//removes first object from chain
	shift: {
		value: function (ch) {
			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain');

			return ch[chi].shift();
		}
	},
	//removes last object from chain
	rol: {
		value: function (ch, n = 1) {
			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain');

			if (n > 0) {
				for (let i = 0; i < n; i ++) {
					ch[chi].push(ch[chi].shift());
				}
			} else if (n < 0) {
				for (let i = 0; i > n; i --) {
					ch[chi].unshift(ch[chi].pop());
				}
			}

			return [...ch[chi]];

		}
	}

});
<commit_msg>style cleanup<commit_after>/*
MIT License

Copyright (c) 2016 jrvieira

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

// export default chain // import chain from 'chainchainchain'
module.exports = chain

// every setting defaults to false
var settings = Object.preventExtensions({
	owncontext: false, // functions remain in owner's context
	//uniqueness: false, // prevent duplicate objects in chain
	allowloops: false, // allow loops (only happens when chaining Ch objects)
	setchained: false // values are set on its youngest owner in chain
})

const chi = Symbol('chi')

var handler = {
	get: function (target, prop, receiver) {

		if (prop === chi) return target[chi]
		// get youngest
		var avoid = chain.findloop(target)

		if (!settings.allowloops && avoid) throw new Error('Loop in chain', avoid)

		for (let o of target[chi]) {

			if (avoid && avoid === o) return false

			if (typeof o[prop] !== 'undefined') {
				// x && y => if x is true returns y, else returns x
				return o[prop] instanceof Function ? o[prop].bind(!settings.owncontext && receiver || o) : o[prop]
			}
		}

		return undefined
	},
	set: function (target, prop, value) {

		if (prop === chi) return target[chi] = value

		var origin = target[chi][0]

		if (!settings.setchained) return origin[prop] = value
		// set youngest
		for (let o of target[chi]) {
			if (typeof o[prop] !== 'undefined') {
				return o[prop] = value
			}
		}

		console.warn(prop+' is not a chained property')
		return false
	}
}

function Ch (arr) {
	this[chi] = arr
}

function chain (...oo) {
	// validate o
	if (typeof oo === 'undefined') throw new TypeError('Argument undefined')

	let arr = (oo.length) ? oo : [{}]
	// validate ch
	console.log('//  ch init')

	var ch = new Proxy(new Ch(arr), handler)
	// find loops
	if (chain.findloop(ch)) {
		if (!settings.allowloops) throw new Error('Loop in chain', chain.findloop(ch))
		console.warn('Loop in chain', chain.findloop(ch))
	}

	return ch
}

Object.defineProperties(chain, {
	// settings object
	set: {
		value: new Proxy(settings, {
			get: function (target, prop) {

				return !!target[prop]
			},
			set: function (target, prop, value) {
				//if (!(prop in target)) throw new Error(prop+' is not a setting')
				if (typeof value !== 'boolean') console.warn('Chain settings should be Boolean, assuming '+!!value+' for '+prop)

				return target[prop] = !!value
			}
		})
	},
	// mind that if you create a loop there is a RangeError: Maximum call stack size exceeded
	findloop: {
		value: function (ch, set = new Set()) {

			set.add(ch)

			for (let o of ch[chi]) {
				if (chain.is(o) && (set.has(o) || chain.findloop(o, set))) {
					return o
				}
			}

			return false
		}
	},
	// returns true if object is chain
	is: {
		value: function (ch) {

			return ch instanceof Ch ? true : false
		}
	},
	// returns origin object
	origin: {
		value: function (ch) {

			return ch[chi][0] || false
		}
	},
	// returns chain's array of objects
	arr: {
		value: function (ch) {

			return ch[chi] ? [...ch[chi]] : false
		}
	},
	// returns array of properties in chain
	raw: {
		value: function (ch, prop, callback) {

			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain')
			// validate p
			if (typeof prop === 'undefined') throw new TypeError('Argument undefined')

			var raw = []
			var own = []

			for (let o of ch[chi]) {
				// x && y => if x is true returns y, else returns x
				raw.push(o[prop] instanceof Function ? o[prop].bind(!settings.owncontext && ch[chi][0] || o) : o[prop])
				if (callback) own.push(o)
			}

			return callback ? callback(raw, own) : raw
		}
	},
	// MANIPULATION
	// adds objects to end of chain
	add: {
		value: function (ch, oo = []) {

			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain')

			if (!(oo instanceof Array)) oo = [oo]
			// appends objects to chain
			ch[chi].push(...oo)
			// find loops
			if (chain.findloop(ch)) {
				if (!settings.allowloops) throw new Error('Loop in chain', chain.findloop(ch))
				console.warn('Loop in chain', chain.findloop(ch))
			}

			return [...ch[chi]]
		}
	},
	// adds objects to beggining of chain
	pre: {
		value: function (ch, oo = []) {

			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain')

			if (!(oo instanceof Array)) oo = [oo]
			// prepends objects to chain
			ch[chi].unshift(...oo)
			// find loops
			if (chain.findloop(ch)) {
				if (!settings.allowloops) throw new Error('Loop in chain', chain.findloop(ch))
				console.warn('Loop in chain', chain.findloop(ch))
			}

			return [...ch[chi]]
		}
	},
	// replaces objects from chain
	rep: {
		value: function (ch, x, o) {

			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain')
			// replaces objects in chain
			while (!(ch[chi].indexOf(x) < 0)) {
				ch[chi].splice(ch[chi].indexOf(x),1,o)
			}
			// find loops
			if (chain.findloop(ch)) {
				if (!settings.allowloops) throw new Error('Loop in chain', chain.findloop(ch))
				console.warn('Loop in chain', chain.findloop(ch))
			}

			return [...ch[chi]]
		}
	},
	// removes objects from chain
	rem: {
		value: function (ch, xx = []) {

			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain')

			if (!(xx instanceof Array)) xx = [xx]
			// filters objects out of chain
			ch[chi] = ch[chi].filter(function (o) {
				return !xx.includes(o)
			})

			return [...ch[chi]]
		}
	},
	// removes last object from chain
	pop: {
		value: function (ch) {

			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain')

			return ch[chi].pop()
		}
	},
	// removes first object from chain
	shift: {
		value: function (ch) {

			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain')

			return ch[chi].shift()
		}
	},
	// removes last object from chain
	rol: {
		value: function (ch, n = 1) {

			if (!(ch instanceof Ch)) throw new TypeError(ch+' is not a chain')

			if (n > 0) {
				for (let i = 0 i < n i ++) {
					ch[chi].push(ch[chi].shift())
				}
			} else if (n < 0) {
				for (let i = 0 i > n i --) {
					ch[chi].unshift(ch[chi].pop())
				}
			}

			return [...ch[chi]]
		}
	}

})
<|endoftext|>


<commit_before>// @flow

import * as React from 'react'
import moment from 'moment'

type Props = {
	interval: number, // ms
	render: ({now: Date}) => React.Node,
	moment?: boolean,
	timezone?: string,
}

type State = {
	now: Date,
}

export class Timer extends React.Component<Props, State> {
	_intervalId: ?IntervalID
	_timeoutId: ?TimeoutID

	state = {
		now: new Date(),
	}

	componentDidMount() {
		this._intervalId = setInterval(this.updateTime, this.props.interval)
		// get the time remaining until the next $interval
		let {interval} = this.props
		let nowMs = this.state.now.getTime()
		let untilNextInterval = interval - (nowMs % interval)

		this._timeoutId = setTimeout(() => {
			this.updateTime()
			this._intervalId = setInterval(this.updateTime, this.props.interval)
		}, untilNextInterval)
	}

	componentWillUnmount() {
		if (this._timeoutId) {
			clearTimeout(this._timeoutId)
		}

		if (this._intervalId) {
			clearInterval(this._intervalId)
		}
	}

	updateTime = () => {
		this.setState(() => ({now: new Date()}))
	}

	render() {
		let {now} = this.state

		if (this.props.moment) {
			now = moment(now)
		}

		if (this.props.timezone) {
			now = now.tz(this.props.timezone)
		}

		return this.props.render({now})
	}
}
<commit_msg>allow forcibly refreshing the Timer<commit_after>// @flow

import * as React from 'react'
import moment from 'moment-timezone'
import delay from 'delay'

type Props = {
	interval: number, // ms
	timezone?: string,
} & (
	| {
			moment?: true,
			render: ({
				now: moment,
				loading: boolean,
				refresh: () => mixed,
			}) => React.Node,
	  }
	| {
			moment?: false,
			render: ({
				now: Date,
				loading: boolean,
				refresh: () => mixed,
			}) => React.Node,
	  }
)

type State = {
	now: Date,
	loading: boolean,
}

export class Timer extends React.Component<Props, State> {
	_intervalId: ?IntervalID
	_timeoutId: ?TimeoutID

	state = {
		now: new Date(),
		loading: false,
	}

	componentDidMount() {
		this._intervalId = setInterval(this.updateTime, this.props.interval)

		// get the time remaining until the next $interval
		let {interval} = this.props
		let nowMs = this.state.now.getTime()
		let untilNextInterval = interval - (nowMs % interval)

		this._timeoutId = setTimeout(() => {
			this.updateTime()
			this._intervalId = setInterval(this.updateTime, this.props.interval)
		}, untilNextInterval)
	}

	componentWillUnmount() {
		if (this._timeoutId) {
			clearTimeout(this._timeoutId)
		}

		if (this._intervalId) {
			clearInterval(this._intervalId)
		}
	}

	updateTime = () => {
		this.setState(() => ({now: new Date()}))
	}

	refresh = async () => {
		const start = Date.now()
		this.setState(() => ({loading: true}))

		this.updateTime()

		const elapsed = Date.now() - start
		if (elapsed < 500) {
			await delay(500 - elapsed)
		}

		this.setState(() => ({loading: false}))
	}

	render() {
		let {now, loading} = this.state

		if (this.props.moment) {
			now = moment(now)

			if (this.props.timezone) {
				now = now.tz(this.props.timezone)
			}
		}

		return this.props.render({now, loading, refresh: this.refresh})
	}
}
<|endoftext|>