<commit_before>/**
 * Plugin for highlighting. It set a lower opacity for other series than the one that is hovered over.
 * Additionally, if not hovering over, a lower opacity is set based on series selected status.
 */
(function (Highcharts) {
    function highlightOn(allSeries, currentSeries) {
        return function (e) {
            _.each(allSeries, function (series, i) {
                if (i === 0) {
                    // We skip (empty) navigator series
                    assert.equal(series.data.length, 0);
                    return;
                }

                var current = series === currentSeries || series.linkedParent === currentSeries || series.options.streamId === currentSeries.options.streamId;
                _.each(['group', 'markerGroup'], function (group, j) {
                    series[group].attr('opacity', current ? 1.0 : 0.25);
                });
                // We reuse visibility styles here
                series.chart.legend.colorizeItem(series, current);
            });
        };
    }

    function highlightOff(allSeries, currentSeries) {
        return function (e) {
            _.each(allSeries, function (series, i) {
                if (i === 0) {
                    // We skip (empty) navigator series
                    assert.equal(series.data.length, 0);
                    return;
                }

                assert(series.options.streamId);

                var selected = series.selected || _.some(_.filter(allSeries, function (s) {return s.options.streamId === series.options.streamId}), function (s) {return s.selected});

                _.each(['group', 'markerGroup'], function (group, j) {
                    series[group].attr('opacity', selected ? 1.0 : 0.25);
                });
                // We reuse visibility styles here
                series.chart.legend.colorizeItem(series, selected);
            });
        };
    }

    // Hovering over the legend
    Highcharts.wrap(Highcharts.Legend.prototype, 'renderItem', function (proceed, currentSeries) {
        proceed.call(this, currentSeries);

        var allSeries = this.chart.series;

        $(currentSeries.legendGroup.element).off('.highlight').on(
            'mouseenter.highlight', highlightOn(allSeries, currentSeries)
        ).on(
            'mouseleave.highlight', highlightOff(allSeries, currentSeries)
        );
    });
}(Highcharts));

// TODO: This curently does not depend on how many datapoints are really available, so if granularity is seconds, it assumes that every second will have a datapoint
// TODO: Should this depend on possible granularity for the stream(s)? Or some other hint?
var MAX_POINTS_NUMBER = 300;
var MAX_DETAIL_LIMIT = 10000;

var GRANULARITIES = [
    {'name': 'days', 'duration': 24 * 60 * 60},
    {'name': '6hours', 'duration': 6 * 60 * 60},
    {'name': 'hours', 'duration': 60 * 60},
    {'name': '10minutes', 'duration': 10 * 60},
    {'name': 'minutes', 'duration': 60},
    {'name': '10seconds', 'duration': 10},
    {'name': 'seconds', 'duration': 1}
];

function firstDefined(obj) {
    for (var i = 1; i < arguments.length; i++) {
        if (!_.isUndefined(obj[arguments[i]])) {
            return obj[arguments[i]];
        }
    }
}

function Stream(stream) {
    var self = this;

    _.extend(self, stream);

    self.initializeChart(function () {
        self.loadInitialData();
    });
}

Stream.prototype.initializeChart = function (callback) {
    var self = this;

    $('
').addClass('chart').appendTo('#charts').highcharts('StockChart', {
        'chart': {
            'zoomType': 'x',
            'borderRadius': 10
        },
        'credits': {
            'enabled': false
        },
        'navigator': {
            'enabled': true,
            'adaptToUpdatedData': false,
            'series': {
                'id': 'navigator',
                // We will add our own series on top of this one and leave this one empty
                'data': []
            }
        },
        'scrollbar': {
            'enabled': true,
            'liveRedraw': false
        },
        'legend': {
            'enabled': true,
            'verticalAlign': 'top',
            'floating': true,
            'padding': 5
        },
        'rangeSelector': {
            'buttonTheme': {
                'width': 50
            },
            'buttons': [
                {
                    'type': 'day',
                    'count': 1,
                    'text': "day"
                },
                {
                    'type': 'week',
                    'count': 1,
                    'text': "week"
                },
                {
                    'type': 'month',
                    'count': 1,
                    'text': "month"
                },
                {
                    'type': 'year',
                    'count': 1,
                    'text': "year"
                },
                {
                    'type': 'all',
                    'text': "all"
                }
            ],
            'selected': 4 // All
        },
        'xAxis': {
            'id': 'x-axis',
            'events': {
                'afterSetExtremes': function (event) {
                    if (event.syncing) {
                        // It is our event and we are syncing extremes between charts, load data for this chart
                        self.loadData(event);
                    }
                    else {
                        // User changed extremes, first sync all charts
                        page.setExtremes(event);
                    }
                }
            },
            'ordinal': false,
            'minRange': MAX_POINTS_NUMBER * 1000 // TODO: Should this depend on possible granularity for the stream(s)? Or some other hint?
        },
        'yAxis': [],
        'plotOptions': {
            'series': {
                'marker': {
                    'enabled': true,
                    'radius': 3
                },
                'dataGrouping': {
                    'enabled': false
                }
            }
        },
        'series': []
    }, function (chart) {
        self.chart = chart;
        self.chart.loadingShown = 0;
        if (callback) callback();
    });
};

Stream.prototype.showLoading = function () {
    var self = this;

    assert(self.chart.loadingShown >= 0);

    self.chart.loadingShown++;
    if (self.chart.loadingShown === 1) {
        self.chart.showLoading("Loading data from server...");
    }
};

Stream.prototype.hideLoading = function () {
    var self = this;

    assert(self.chart.loadingShown > 0);

    self.chart.loadingShown--;
    if (self.chart.loadingShown === 0) {
        self.chart.hideLoading();
    }
};

Stream.prototype.convertDatapoint = function (datapoint) {
    var self = this;

    // TODO: Convert based on visualization tags

    var t = moment.utc(_.isObject(datapoint.t) ? datapoint.t.m : datapoint.t).valueOf();
    if (_.isObject(datapoint.v)) {
        return {
            'line': [t, datapoint.v.m],
            'range': [t, datapoint.v.l, datapoint.v.u]
        }
    }
    else {
        return {
            'line': [t, datapoint.v],
            'range': [t, datapoint.v, datapoint.v]
        }
    }
};

Stream.prototype.convertDatapoints = function (datapoints) {
    var self = this;

    // TODO: Convert based on visualization tags

    var line = [];
    var range = [];

    _.each(datapoints, function (datapoint, i) {
        datapoint = self.convertDatapoint(datapoint);
        line.push(datapoint.line);
        range.push(datapoint.range);
    });

    return {
        'line': line,
        'range': range
    };
};

Stream.prototype.loadInitialData = function () {
    var self = this;

    self.showLoading();

    $.getJSON(self.resource_uri, {
        // Use lowest granularity and no bounds to get initial data
        'granularity': GRANULARITIES[0].name,
        // We want to get all we can, we are loading days so it should not be so bad
        'limit': MAX_DETAIL_LIMIT,
        'value_downsamplers': self.valueDownsamplers(true),
        'time_downsamplers': self.timeDownsamplers(true)
    }, function (data, textStatus, jqXHR) {
        assert.equal(data.id, self.id);

        var datapoints = self.convertDatapoints(data.datapoints);

        var firstSeries = self.chart.series.length <= 1;

        self.chart.addAxis({
            'id': 'y-axis-' + self.id,
            'title': {
                'text': [self.tags.unit_description || "", self.tags.unit ? "[" + self.tags.unit + "]" : ""].join(" ")
            },
            'showEmpty': false
        });
        var series = self.chart.addSeries({
            'id': 'range-' + self.id,
            'streamId': self.id, // Our own option
            'name': self.tags.title,
            'yAxis': 'y-axis-' + self.id,
            'type': 'arearange',
            'lineWidth': 0,
            'fillOpacity': 0.3,
            'tooltip': {
                'pointFormat': '{series.name} min/max: {point.low} - {point.high}
',
                'valueDecimals': 3
            },
            'selected': true, // By default all streams are selected/highlighted
            'events': {
                'legendItemClick': function (e) {
                    e.preventDefault();

                    this.select();

                    // We force mouse leave event to immediately set highlights
                    $(this.legendGroup.element).trigger('mouseleave.highlight');
                }
            },
            'data': datapoints.range
        });
        // Match yAxis title color with series color
        series.yAxis.axisTitle.css({'color': series.color});
        self.chart.addSeries({
            'id': 'line-' + self.id,
            'streamId': self.id, // Our own option
            'name': self.tags.title,
            'linkedTo': 'range-' + self.id,
            'yAxis': 'y-axis-' + self.id,
            'type': 'spline',
            'color': series.color,
            'tooltip': {
                'pointFormat': '{series.name} mean: {point.y}
',
                'valueDecimals': 3
            },
            'data': datapoints.line
        });
        var navigator = self.chart.get('navigator');
        self.chart.addAxis(_.extend({}, navigator.yAxis.options, {
            'id': 'navigator-y-axis-' + self.id
        }));
        self.chart.addSeries(_.extend({}, navigator.options, {
            'id': 'navigator-' + self.id,
            'streamId': self.id, // Our own option
            'color': series.color,
            'data': datapoints.line,
            'yAxis': 'navigator-y-axis-' + self.id
        }));

        if (firstSeries) {
            // Without the following range selector is not displayed until first zooming
            self.chart.xAxis[0].setExtremes();
        }

        page.updateKnownMaxRange(data);
    }).always(function () {
        self.hideLoading();
    });
};

Stream.prototype.computeRange = function (min, max) {
    var self = this;

    var range = {
        'granularity': GRANULARITIES[0]
    };

    if (!_.isNumber(min) || !_.isNumber(min)) {
        return range;
    }

    // In JavaScript timestamps are in milliseconds, but server sides uses them in seconds
    range.start = min / 1000;
    range.end = max / 1000;

    var interval = range.end - range.start;

    _.each(GRANULARITIES, function (granularity, i) {
        if (interval / granularity.duration > MAX_POINTS_NUMBER) {
            return false;
        }
        range.granularity = granularity;
    });

    // We enlarge range for 10 % in each direction
    range.start -= interval * 0.1;
    range.end += interval * 0.1;

    range.start = parseInt(Math.floor(range.start));
    range.end = parseInt(Math.ceil(range.end));

    return range;
};

Stream.prototype.valueDownsamplers = function (initial) {
    var self = this;

    return _.union(self.tags.visualization.value_downsamplers, initial ? ['mean'] : [])
};

Stream.prototype.timeDownsamplers = function (initial) {
    var self = this;

    return _.union(self.tags.visualization.time_downsamplers, initial ? ['first', 'last'] : [])
};

Stream.prototype.loadData = function (event) {
    var self = this;

    var range = self.computeRange(event.min, event.max);

    self.showLoading();
    $.getJSON(self.resource_uri, {
        'granularity': range.granularity.name,
        'limit': 1000, // TODO: How much exactly do we want?
        'start': range.start,
        'end': range.end,
        'value_downsamplers': self.valueDownsamplers(),
        'time_downsamplers': self.timeDownsamplers()
    }, function (data, textStatus, jqXHR) {
        assert.equal(data.id, self.id);

        // TODO: Convert based on visualization tags
        var datapoints = self.convertDatapoints(data.datapoints);
        self.chart.get('line-' + self.id).setData(datapoints.line);
        self.chart.get('range-' + self.id).setData(datapoints.range);

        self.hideLoading();

        page.updateKnownMaxRange(data);
    }).fail(function () {
        self.hideLoading();
    });
};

function Page() {
    var self = this;

    self.streams = {};
    self.minRange = null;
    self.maxRange = null;
}

Page.prototype.newStream = function (stream) {
    var self = this;

    assert(!_.has(self.streams, stream.id));

    self.streams[stream.id] = new Stream(stream);
};

Page.prototype.setExtremes = function (event) {
    var self = this;

    self._setExtremes(event.min, event.max, 'x-axis');
};

Page.prototype._setExtremes = function (min, max, axis) {
    var self = this;

    var charts =_.uniq(_.pluck(_.values(self.streams), 'chart'));

    _.each(charts, function (chart, i) {
        // We set "syncing" flag on the event so that charts know that they have to load data now
        chart.get(axis).setExtremes(min, max, true, false, {'syncing': true});
    });
};

Page.prototype.updateKnownMaxRange = function (data) {
    var self = this;

    assert(_.has(self.streams, data.id));

    if (!data.datapoints || data.datapoints.length === 0) {
        return;
    }

    var firstDatapoint = data.datapoints[0];
    var lastDatapoint = data.datapoints[data.datapoints.length - 1];

    // We go through downsampled timestamps in such order to maximize the range
    var start = _.isObject(firstDatapoint.t) ? firstDefined(firstDatapoint.t, 'a', 'e', 'm', 'z') : firstDatapoint.t;
    var end = _.isObject(lastDatapoint.t) ? firstDefined(lastDatapoint.t, 'z', 'm', 'e', 'a') : lastDatapoint.t;

    var changed = false;

    if (!_.isUndefined(start) && (self.minRange === null || moment.utc(start).valueOf() < self.minRange)) {
        changed = true;
        self.minRange = moment.utc(start).valueOf();
    }
    if (!_.isUndefined(end) && (self.maxRange === null || moment.utc(end).valueOf() > self.maxRange)) {
        changed = true;
        self.maxRange = moment.utc(end).valueOf();
    }

    if (changed && self.minRange !== null && self.maxRange !== null) {
        self._setExtremes(self.minRange, self.maxRange, 'navigator-x-axis');
    }
};

var page = new Page();

$(document).ready(function () {
    $.ajaxSetup({
        'traditional': true
    });

    $(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {
        console.error(event, jqXHR, ajaxSettings, thrownError);
    });

    // TODO: Will load only the first page of streams
    // TODO: Allow some way of filtering streams
    $.getJSON('/api/v1/stream/', function (data, textStatus, jqXHR) {
        _.each(data.objects, function (stream, i) {
            page.newStream(stream);
        });
    });
});<commit_msg>Don't do unnecessary requests.<commit_after>/**
 * Plugin for highlighting. It set a lower opacity for other series than the one that is hovered over.
 * Additionally, if not hovering over, a lower opacity is set based on series selected status.
 */
(function (Highcharts) {
    function highlightOn(allSeries, currentSeries) {
        return function (e) {
            _.each(allSeries, function (series, i) {
                if (i === 0) {
                    // We skip (empty) navigator series
                    assert.equal(series.data.length, 0);
                    return;
                }

                var current = series === currentSeries || series.linkedParent === currentSeries || series.options.streamId === currentSeries.options.streamId;
                _.each(['group', 'markerGroup'], function (group, j) {
                    series[group].attr('opacity', current ? 1.0 : 0.25);
                });
                // We reuse visibility styles here
                series.chart.legend.colorizeItem(series, current);
            });
        };
    }

    function highlightOff(allSeries, currentSeries) {
        return function (e) {
            _.each(allSeries, function (series, i) {
                if (i === 0) {
                    // We skip (empty) navigator series
                    assert.equal(series.data.length, 0);
                    return;
                }

                assert(series.options.streamId);

                var selected = series.selected || _.some(_.filter(allSeries, function (s) {return s.options.streamId === series.options.streamId}), function (s) {return s.selected});

                _.each(['group', 'markerGroup'], function (group, j) {
                    series[group].attr('opacity', selected ? 1.0 : 0.25);
                });
                // We reuse visibility styles here
                series.chart.legend.colorizeItem(series, selected);
            });
        };
    }

    // Hovering over the legend
    Highcharts.wrap(Highcharts.Legend.prototype, 'renderItem', function (proceed, currentSeries) {
        proceed.call(this, currentSeries);

        var allSeries = this.chart.series;

        $(currentSeries.legendGroup.element).off('.highlight').on(
            'mouseenter.highlight', highlightOn(allSeries, currentSeries)
        ).on(
            'mouseleave.highlight', highlightOff(allSeries, currentSeries)
        );
    });
}(Highcharts));

// TODO: This curently does not depend on how many datapoints are really available, so if granularity is seconds, it assumes that every second will have a datapoint
// TODO: Should this depend on possible granularity for the stream(s)? Or some other hint?
var MAX_POINTS_NUMBER = 300;
var MAX_DETAIL_LIMIT = 10000;

var GRANULARITIES = [
    {'name': 'days', 'duration': 24 * 60 * 60},
    {'name': '6hours', 'duration': 6 * 60 * 60},
    {'name': 'hours', 'duration': 60 * 60},
    {'name': '10minutes', 'duration': 10 * 60},
    {'name': 'minutes', 'duration': 60},
    {'name': '10seconds', 'duration': 10},
    {'name': 'seconds', 'duration': 1}
];

function firstDefined(obj) {
    for (var i = 1; i < arguments.length; i++) {
        if (!_.isUndefined(obj[arguments[i]])) {
            return obj[arguments[i]];
        }
    }
}

function Stream(stream) {
    var self = this;

    _.extend(self, stream);

    self.lastRangeStart = null;
    self.lastRangeEnd = null;

    self.initializeChart(function () {
        self.loadInitialData();
    });
}

Stream.prototype.initializeChart = function (callback) {
    var self = this;

    $('
').addClass('chart').appendTo('#charts').highcharts('StockChart', {
        'chart': {
            'zoomType': 'x',
            'borderRadius': 10
        },
        'credits': {
            'enabled': false
        },
        'navigator': {
            'enabled': true,
            'adaptToUpdatedData': false,
            'series': {
                'id': 'navigator',
                // We will add our own series on top of this one and leave this one empty
                'data': []
            }
        },
        'scrollbar': {
            'enabled': true,
            'liveRedraw': false
        },
        'legend': {
            'enabled': true,
            'verticalAlign': 'top',
            'floating': true,
            'padding': 5
        },
        'rangeSelector': {
            'buttonTheme': {
                'width': 50
            },
            'buttons': [
                {
                    'type': 'day',
                    'count': 1,
                    'text': "day"
                },
                {
                    'type': 'week',
                    'count': 1,
                    'text': "week"
                },
                {
                    'type': 'month',
                    'count': 1,
                    'text': "month"
                },
                {
                    'type': 'year',
                    'count': 1,
                    'text': "year"
                },
                {
                    'type': 'all',
                    'text': "all"
                }
            ],
            'selected': 4 // All
        },
        'xAxis': {
            'id': 'x-axis',
            'events': {
                'afterSetExtremes': function (event) {
                    if (event.syncing) {
                        // It is our event and we are syncing extremes between charts, load data for this chart
                        self.loadData(event);
                    }
                    else {
                        // User changed extremes, first sync all charts
                        page.setExtremes(event);
                    }
                }
            },
            'ordinal': false,
            'minRange': MAX_POINTS_NUMBER * 1000 // TODO: Should this depend on possible granularity for the stream(s)? Or some other hint?
        },
        'yAxis': [],
        'plotOptions': {
            'series': {
                'marker': {
                    'enabled': true,
                    'radius': 3
                },
                'dataGrouping': {
                    'enabled': false
                }
            }
        },
        'series': []
    }, function (chart) {
        self.chart = chart;
        self.chart.loadingShown = 0;
        if (callback) callback();
    });
};

Stream.prototype.showLoading = function () {
    var self = this;

    assert(self.chart.loadingShown >= 0);

    self.chart.loadingShown++;
    if (self.chart.loadingShown === 1) {
        self.chart.showLoading("Loading data from server...");
    }
};

Stream.prototype.hideLoading = function () {
    var self = this;

    assert(self.chart.loadingShown > 0);

    self.chart.loadingShown--;
    if (self.chart.loadingShown === 0) {
        self.chart.hideLoading();
    }
};

Stream.prototype.convertDatapoint = function (datapoint) {
    var self = this;

    // TODO: Convert based on visualization tags

    var t = moment.utc(_.isObject(datapoint.t) ? datapoint.t.m : datapoint.t).valueOf();
    if (_.isObject(datapoint.v)) {
        return {
            'line': [t, datapoint.v.m],
            'range': [t, datapoint.v.l, datapoint.v.u]
        }
    }
    else {
        return {
            'line': [t, datapoint.v],
            'range': [t, datapoint.v, datapoint.v]
        }
    }
};

Stream.prototype.convertDatapoints = function (datapoints) {
    var self = this;

    // TODO: Convert based on visualization tags

    var line = [];
    var range = [];

    _.each(datapoints, function (datapoint, i) {
        datapoint = self.convertDatapoint(datapoint);
        line.push(datapoint.line);
        range.push(datapoint.range);
    });

    return {
        'line': line,
        'range': range
    };
};

Stream.prototype.loadInitialData = function () {
    var self = this;

    self.showLoading();

    $.getJSON(self.resource_uri, {
        // Use lowest granularity and no bounds to get initial data
        'granularity': GRANULARITIES[0].name,
        // We want to get all we can, we are loading days so it should not be so bad
        'limit': MAX_DETAIL_LIMIT,
        'value_downsamplers': self.valueDownsamplers(true),
        'time_downsamplers': self.timeDownsamplers(true)
    }, function (data, textStatus, jqXHR) {
        assert.equal(data.id, self.id);

        var datapoints = self.convertDatapoints(data.datapoints);

        var firstSeries = self.chart.series.length <= 1;

        self.chart.addAxis({
            'id': 'y-axis-' + self.id,
            'title': {
                'text': [self.tags.unit_description || "", self.tags.unit ? "[" + self.tags.unit + "]" : ""].join(" ")
            },
            'showEmpty': false
        });
        var series = self.chart.addSeries({
            'id': 'range-' + self.id,
            'streamId': self.id, // Our own option
            'name': self.tags.title,
            'yAxis': 'y-axis-' + self.id,
            'type': 'arearange',
            'lineWidth': 0,
            'fillOpacity': 0.3,
            'tooltip': {
                'pointFormat': '{series.name} min/max: {point.low} - {point.high}
',
                'valueDecimals': 3
            },
            'selected': true, // By default all streams are selected/highlighted
            'events': {
                'legendItemClick': function (e) {
                    e.preventDefault();

                    this.select();

                    // We force mouse leave event to immediately set highlights
                    $(this.legendGroup.element).trigger('mouseleave.highlight');
                }
            },
            'data': datapoints.range
        });
        // Match yAxis title color with series color
        series.yAxis.axisTitle.css({'color': series.color});
        self.chart.addSeries({
            'id': 'line-' + self.id,
            'streamId': self.id, // Our own option
            'name': self.tags.title,
            'linkedTo': 'range-' + self.id,
            'yAxis': 'y-axis-' + self.id,
            'type': 'spline',
            'color': series.color,
            'tooltip': {
                'pointFormat': '{series.name} mean: {point.y}
',
                'valueDecimals': 3
            },
            'data': datapoints.line
        });
        var navigator = self.chart.get('navigator');
        self.chart.addAxis(_.extend({}, navigator.yAxis.options, {
            'id': 'navigator-y-axis-' + self.id
        }));
        self.chart.addSeries(_.extend({}, navigator.options, {
            'id': 'navigator-' + self.id,
            'streamId': self.id, // Our own option
            'color': series.color,
            'data': datapoints.line,
            'yAxis': 'navigator-y-axis-' + self.id
        }));

        if (firstSeries) {
            // Without the following range selector is not displayed until first zooming
            self.chart.xAxis[0].setExtremes();
        }

        page.updateKnownMaxRange(data);
    }).always(function () {
        self.hideLoading();
    });
};

Stream.prototype.computeRange = function (min, max) {
    var self = this;

    var range = {
        'granularity': GRANULARITIES[0]
    };

    if (!_.isNumber(min) || !_.isNumber(min)) {
        return range;
    }

    // In JavaScript timestamps are in milliseconds, but server sides uses them in seconds
    range.start = min / 1000;
    range.end = max / 1000;

    var interval = range.end - range.start;

    _.each(GRANULARITIES, function (granularity, i) {
        if (interval / granularity.duration > MAX_POINTS_NUMBER) {
            return false;
        }
        range.granularity = granularity;
    });

    // We enlarge range for 10 % in each direction
    range.start -= interval * 0.1;
    range.end += interval * 0.1;

    range.start = parseInt(Math.floor(range.start));
    range.end = parseInt(Math.ceil(range.end));

    return range;
};

Stream.prototype.valueDownsamplers = function (initial) {
    var self = this;

    return _.union(self.tags.visualization.value_downsamplers, initial ? ['mean'] : [])
};

Stream.prototype.timeDownsamplers = function (initial) {
    var self = this;

    return _.union(self.tags.visualization.time_downsamplers, initial ? ['first', 'last'] : [])
};

Stream.prototype.loadData = function (event) {
    var self = this;

    var range = self.computeRange(event.min, event.max);

    if (range.start === self.lastRangeStart && range.end === self.lastRangeEnd) {
        // Nothing really changed
        return;
    }

    self.showLoading();
    $.getJSON(self.resource_uri, {
        'granularity': range.granularity.name,
        'limit': 1000, // TODO: How much exactly do we want?
        'start': range.start,
        'end': range.end,
        'value_downsamplers': self.valueDownsamplers(),
        'time_downsamplers': self.timeDownsamplers()
    }, function (data, textStatus, jqXHR) {
        assert.equal(data.id, self.id);

        self.lastRangeStart = range.start;
        self.lastRangeEnd = range.end;

        // TODO: Convert based on visualization tags
        var datapoints = self.convertDatapoints(data.datapoints);
        self.chart.get('line-' + self.id).setData(datapoints.line);
        self.chart.get('range-' + self.id).setData(datapoints.range);

        self.hideLoading();

        page.updateKnownMaxRange(data);
    }).fail(function () {
        self.hideLoading();
    });
};

function Page() {
    var self = this;

    self.streams = {};
    self.minRange = null;
    self.maxRange = null;
}

Page.prototype.newStream = function (stream) {
    var self = this;

    assert(!_.has(self.streams, stream.id));

    self.streams[stream.id] = new Stream(stream);
};

Page.prototype.setExtremes = function (event) {
    var self = this;

    self._setExtremes(event.min, event.max, 'x-axis');
};

Page.prototype._setExtremes = function (min, max, axis) {
    var self = this;

    var charts =_.uniq(_.pluck(_.values(self.streams), 'chart'));

    _.each(charts, function (chart, i) {
        // We set "syncing" flag on the event so that charts know that they have to load data now
        chart.get(axis).setExtremes(min, max, true, false, {'syncing': true});
    });
};

Page.prototype.updateKnownMaxRange = function (data) {
    var self = this;

    assert(_.has(self.streams, data.id));

    if (!data.datapoints || data.datapoints.length === 0) {
        return;
    }

    var firstDatapoint = data.datapoints[0];
    var lastDatapoint = data.datapoints[data.datapoints.length - 1];

    // We go through downsampled timestamps in such order to maximize the range
    var start = _.isObject(firstDatapoint.t) ? firstDefined(firstDatapoint.t, 'a', 'e', 'm', 'z') : firstDatapoint.t;
    var end = _.isObject(lastDatapoint.t) ? firstDefined(lastDatapoint.t, 'z', 'm', 'e', 'a') : lastDatapoint.t;

    var changed = false;

    if (!_.isUndefined(start) && (self.minRange === null || moment.utc(start).valueOf() < self.minRange)) {
        changed = true;
        self.minRange = moment.utc(start).valueOf();
    }
    if (!_.isUndefined(end) && (self.maxRange === null || moment.utc(end).valueOf() > self.maxRange)) {
        changed = true;
        self.maxRange = moment.utc(end).valueOf();
    }

    if (changed && self.minRange !== null && self.maxRange !== null) {
        self._setExtremes(self.minRange, self.maxRange, 'navigator-x-axis');
    }
};

var page = new Page();

$(document).ready(function () {
    $.ajaxSetup({
        'traditional': true
    });

    $(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {
        console.error(event, jqXHR, ajaxSettings, thrownError);
    });

    // TODO: Will load only the first page of streams
    // TODO: Allow some way of filtering streams
    $.getJSON('/api/v1/stream/', function (data, textStatus, jqXHR) {
        _.each(data.objects, function (stream, i) {
            page.newStream(stream);
        });
    });
});<|endoftext|>


<commit_before>const { app, Menu, BrowserWindow, Notification, ipcMain } = require('electron');
const path = require('path');
const url = require('url');
const os = require('os');

const appConfig = require(path.join(
  __dirname,
  '/../www/assets/appConfig.json'
));
console.log('Desktop: ' + appConfig.nameCase + ' v' + appConfig.version);

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;

// Deep linked url
let deeplinkingUrl;

const isDevMode = process.execPath.match(/[\\/]electron/);

function createWindow() {
  // Create the browser window.
  win = new BrowserWindow({
    width: 400,
    height: 650,
    minWidth: 400,
    maxWidth: 800,
    minHeight: 650,
    show: false
  });

  // and load the index.html of the app.
  win.loadURL(
    url.format({
      pathname: path.join(__dirname, '/../www/index.html'),
      protocol: 'file:',
      slashes: true
    })
  );

  // Open the DevTools.
  if (isDevMode) {
    win.webContents.openDevTools();
  }
  // Emitted when the window is closed.
  win.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    win = null;
  });

  win.webContents.on('did-finish-load', () => {
    // Windows - Linux: Handle deeplink url
    if (process.platform == 'win32' || process.platform == 'linux') {
      // Keep only command line / deep linked arguments
      deeplinkingUrl = process.argv ? process.argv[1] : null;
    }
    if (deeplinkingUrl) {
      setTimeout(() => {
        win.webContents.send('open-url-event', deeplinkingUrl);
      }, 1000);
    }
    if (Notification.isSupported()) {
      ipcMain.on('new-notification', (event, data) => {
        new Notification(data).show();
      });
    }
  });

  win.once('ready-to-show', () => {
    win.show();
  });
}

function createMenu() {
  const template = [
    {
      label: 'Edit',
      submenu: [
        { role: 'cut' },
        { role: 'copy' },
        { role: 'paste' },
        { role: 'selectall' }
      ]
    },
    {
      role: 'window',
      submenu: [{ role: 'minimize' }, { role: 'close' }]
    },
    {
      role: 'help',
      submenu: [
        {
          label: 'Open Help Center',
          click() {
            require('electron').shell.openExternal(
              'https://support.bitpay.com/hc/en-us'
            );
          }
        }
      ]
    }
  ];

  if (process.platform === 'darwin') {
    template.unshift({
      label: appConfig.nameCase,
      submenu: [
        { role: 'about' },
        { type: 'separator' },
        { role: 'hide' },
        { role: 'hideothers' },
        { role: 'unhide' },
        { type: 'separator' },
        { role: 'quit' }
      ]
    });
  }

  Menu.setApplicationMenu(Menu.buildFromTemplate(template));
}
// The setAsDefaultProtocolClient only works on packaged versions of the application

app.setAsDefaultProtocolClient('bitcoin');
app.setAsDefaultProtocolClient('bitcoincash');
app.setAsDefaultProtocolClient('bchtest');
app.setAsDefaultProtocolClient(appConfig.name);
app.setVersion(appConfig.version);
app.setName(appConfig.nameCase);

const getHomeDirPath = platform => {
  switch (platform) {
    case 'win32':
    case 'darwin':
    case 'linux':
      return os.homedir();
    default:
      throw new Error('Platform not supported');
  }
};
const homeDir = getHomeDirPath(process.platform);

app.setPath('userData', path.join(homeDir, `.${appConfig.name}/app`));

// This method makes your application a Single Instance Application
// https://electronjs.org/docs/api/app#apphassingleinstancelock
const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
  app.quit();
} else {
  app.on('second-instance', (event, argv, workingDirectory) => {
    if (win) {
      // Windows - Linux: Handle deeplink url
      if (process.platform == 'win32' || process.platform == 'linux') {
        deeplinkingUrl = argv ? argv[1] : null;
        if (deeplinkingUrl) {
          win.webContents.send('open-url-event', deeplinkingUrl);
        }
      }
      // Someone tried to run a second instance, we should focus our window.
      if (win.isMinimized()) win.restore();
      win.focus();
    }
  });

  // This method will be called when Electron has finished
  // initialization and is ready to create browser windows.
  // Some APIs can only be used after this event occurs.
  app.on('ready', () => {
    createWindow();
    createMenu();
  });
}

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  // if (process.platform !== 'darwin') {
  app.quit();
  // }
});

app.on('activate', () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (win === null) {
    createWindow();
  }
});

app.on('open-url', function(e, url) {
  e.preventDefault();
  deeplinkingUrl = url;
  // Wait for main window to be ready
  if (win) {
    win.webContents.send('open-url-event', deeplinkingUrl);
    deeplinkingUrl = null;
    if (win.isMinimized()) {
      win.restore();
    } else {
      win.focus();
    }
  }
});

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
<commit_msg>add return when apps quit<commit_after>const { app, Menu, BrowserWindow, Notification, ipcMain } = require('electron');
const path = require('path');
const url = require('url');
const os = require('os');

const appConfig = require(path.join(
  __dirname,
  '/../www/assets/appConfig.json'
));
console.log('Desktop: ' + appConfig.nameCase + ' v' + appConfig.version);

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;

// Deep linked url
let deeplinkingUrl;

const isDevMode = process.execPath.match(/[\\/]electron/);

function createWindow() {
  // Create the browser window.
  win = new BrowserWindow({
    width: 400,
    height: 650,
    minWidth: 400,
    maxWidth: 800,
    minHeight: 650,
    show: false
  });

  // and load the index.html of the app.
  win.loadURL(
    url.format({
      pathname: path.join(__dirname, '/../www/index.html'),
      protocol: 'file:',
      slashes: true
    })
  );

  // Open the DevTools.
  if (isDevMode) {
    win.webContents.openDevTools();
  }
  // Emitted when the window is closed.
  win.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    win = null;
  });

  win.webContents.on('did-finish-load', () => {
    // Windows - Linux: Handle deeplink url
    if (process.platform == 'win32' || process.platform == 'linux') {
      // Keep only command line / deep linked arguments
      deeplinkingUrl = process.argv ? process.argv[1] : null;
    }
    if (deeplinkingUrl) {
      setTimeout(() => {
        win.webContents.send('open-url-event', deeplinkingUrl);
      }, 1000);
    }
    if (Notification.isSupported()) {
      ipcMain.on('new-notification', (event, data) => {
        new Notification(data).show();
      });
    }
  });

  win.once('ready-to-show', () => {
    win.show();
  });
}

function createMenu() {
  const template = [
    {
      label: 'Edit',
      submenu: [
        { role: 'cut' },
        { role: 'copy' },
        { role: 'paste' },
        { role: 'selectall' }
      ]
    },
    {
      role: 'window',
      submenu: [{ role: 'minimize' }, { role: 'close' }]
    },
    {
      role: 'help',
      submenu: [
        {
          label: 'Open Help Center',
          click() {
            require('electron').shell.openExternal(
              'https://support.bitpay.com/hc/en-us'
            );
          }
        }
      ]
    }
  ];

  if (process.platform === 'darwin') {
    template.unshift({
      label: appConfig.nameCase,
      submenu: [
        { role: 'about' },
        { type: 'separator' },
        { role: 'hide' },
        { role: 'hideothers' },
        { role: 'unhide' },
        { type: 'separator' },
        { role: 'quit' }
      ]
    });
  }

  Menu.setApplicationMenu(Menu.buildFromTemplate(template));
}
// The setAsDefaultProtocolClient only works on packaged versions of the application

app.setAsDefaultProtocolClient('bitcoin');
app.setAsDefaultProtocolClient('bitcoincash');
app.setAsDefaultProtocolClient('bchtest');
app.setAsDefaultProtocolClient(appConfig.name);
app.setVersion(appConfig.version);
app.setName(appConfig.nameCase);

const getHomeDirPath = platform => {
  switch (platform) {
    case 'win32':
    case 'darwin':
    case 'linux':
      return os.homedir();
    default:
      throw new Error('Platform not supported');
  }
};
const homeDir = getHomeDirPath(process.platform);

app.setPath('userData', path.join(homeDir, `.${appConfig.name}/app`));

// This method makes your application a Single Instance Application
// https://electronjs.org/docs/api/app#apphassingleinstancelock
const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
  app.quit();
  return;
} else {
  app.on('second-instance', (event, argv, workingDirectory) => {
    if (win) {
      // Windows - Linux: Handle deeplink url
      if (process.platform == 'win32' || process.platform == 'linux') {
        deeplinkingUrl = argv ? argv[1] : null;
        if (deeplinkingUrl) {
          win.webContents.send('open-url-event', deeplinkingUrl);
        }
      }
      // Someone tried to run a second instance, we should focus our window.
      if (win.isMinimized()) win.restore();
      win.focus();
    }
  });

  // This method will be called when Electron has finished
  // initialization and is ready to create browser windows.
  // Some APIs can only be used after this event occurs.
  app.on('ready', () => {
    createWindow();
    createMenu();
  });
}

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  // if (process.platform !== 'darwin') {
  app.quit();
  // }
});

app.on('activate', () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (win === null) {
    createWindow();
  }
});

app.on('open-url', function(e, url) {
  e.preventDefault();
  deeplinkingUrl = url;
  // Wait for main window to be ready
  if (win) {
    win.webContents.send('open-url-event', deeplinkingUrl);
    deeplinkingUrl = null;
    if (win.isMinimized()) {
      win.restore();
    } else {
      win.focus();
    }
  }
});

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
<|endoftext|>


<commit_before>define([
    "jquery",
    "bootstrap",   // needed for modal operations.
    "utils",
    "jsapi/v1/resources"
], function ($, bootstrap, utils, resources) {
    "use strict";
    var base_url = window.apidata.base_url;

    $('#create-new-policy-dialog').on('show.bs.modal', function () {
        var dialog = $(this);
        $(dialog).find(".alert").hide();

        var ok_callback = function () {
            var rep = {
                user_name: $.trim($("#user_name").val()),
                image_name: $.trim($("#image_name").val()),
                allow_home: $("#allow_home").prop("checked"),
                volume_source: $.trim($("#volume_source").val()),
                volume_target: $.trim($("#volume_target").val()),
                volume_readonly: $("#volume_readonly").prop("checked")
            };

            $(dialog).find(".alert").hide();

            if (rep.image_name.length === 0) {
                $(dialog).find("#image_name_alert")
                    .text("Image name cannot be empty")
                    .show();
                return;
            }
            if (rep.volume_source.length !== 0 && rep.volume_source[0] !== '/') {
                $(dialog).find("#volume_source_alert")
                    .text("Must be an absolute path or empty")
                    .show();
                return;
            }

            if (rep.volume_target.length !== 0 && rep.volume_target[0] !== '/') {
                $(dialog).find("#volume_target_alert")
                    .text("Must be an absolute path or empty")
                    .show();
                return;
            }

            if (rep.volume_source.length === 0 && rep.volume_target.length !== 0) {
                $(dialog).find("#volume_source_alert")
                    .text("Must not be empty if target is defined")
                    .show();
                return;
            }

            if (rep.volume_source.length !== 0 && rep.volume_target.length === 0) {
                $(dialog).find("#volume_target_alert")
                    .text("Must not be empty if source is defined")
                    .show();
                return;
            }

            dialog.modal('hide');

            resources.Policies.create(rep)
                .done(function() { window.location.reload(); })
                .fail(utils.ajax_error_dialog);
        };

        var cancel_callback = function () {
            dialog.modal('hide');
        };

        $(dialog).find("form").on('submit', function(e) {
            e.preventDefault();
            ok_callback();
        });

        utils.config_dialog(
            dialog,
            null,
            null,
            ok_callback,
            cancel_callback
        );
    });

    $('#create-new-policy-dialog').on('shown.bs.modal', function () {
        $("#image_name").focus();
    });

    $('#action-dialog').on('show.bs.modal', function (event) {
        var button = $(event.relatedTarget);
        var id = button.data('value');
        var name = button.data('name');
        var dialog = $(this);
        utils.config_dialog(
            dialog,
            'Remove policy ' + name + "?",
            "Do you want to remove policy " + name + "? ",
            function() {
                resources.Policies.delete(id)
                   .done(function() { window.location.reload(); })
                    .fail(utils.ajax_error_dialog);
            }
        );
    });
});
<commit_msg>Using volume string for the representation<commit_after>define([
    "jquery",
    "bootstrap",   // needed for modal operations.
    "utils",
    "jsapi/v1/resources"
], function ($, bootstrap, utils, resources) {
    "use strict";
    var base_url = window.apidata.base_url;

    $('#create-new-policy-dialog').on('show.bs.modal', function () {
        var dialog = $(this);
        $(dialog).find(".alert").hide();

        var ok_callback = function () {
            var user_name = $.trim($("#user_name").val());
            var image_name = $.trim($("#image_name").val());
            var allow_home = $("#allow_home").prop("checked");
            var volume_source = $.trim($("#volume_source").val());
            var volume_target = $.trim($("#volume_target").val());
            var volume_readonly = $("#volume_readonly").prop("checked");

            $(dialog).find(".alert").hide();

            if (image_name.length === 0) {
                $(dialog).find("#image_name_alert")
                    .text("Image name cannot be empty")
                    .show();
                return;
            }
            if (volume_source.length !== 0 && volume_source[0] !== '/') {
                $(dialog).find("#volume_source_alert")
                    .text("Must be an absolute path or empty")
                    .show();
                return;
            }

            if (volume_target.length !== 0 && volume_target[0] !== '/') {
                $(dialog).find("#volume_target_alert")
                    .text("Must be an absolute path or empty")
                    .show();
                return;
            }

            if (volume_source.length === 0 && volume_target.length !== 0) {
                $(dialog).find("#volume_source_alert")
                    .text("Must not be empty if target is defined")
                    .show();
                return;
            }

            if (volume_source.length !== 0 && volume_target.length === 0) {
                $(dialog).find("#volume_target_alert")
                    .text("Must not be empty if source is defined")
                    .show();
                return;
            }

            var volume_string = "";
            if (volume_source.length !== 0 && volume_target.length !== 0) {
                var volume_mode;
                if (volume_readonly) {
                    volume_mode = "ro";
                } else {
                    volume_mode = "rw";
                }
                volume_string = volume_source+":"+volume_target+":"+volume_mode;
            }

            dialog.modal('hide');
            var rep = {
                user_name: user_name,
                image_name: image_name,
                allow_home: allow_home,
                volume: volume_string
            };

            resources.Policies.create(rep)
                .done(function() { window.location.reload(); })
                .fail(utils.ajax_error_dialog);
        };

        var cancel_callback = function () {
            dialog.modal('hide');
        };

        $(dialog).find("form").on('submit', function(e) {
            e.preventDefault();
            ok_callback();
        });

        utils.config_dialog(
            dialog,
            null,
            null,
            ok_callback,
            cancel_callback
        );
    });

    $('#create-new-policy-dialog').on('shown.bs.modal', function () {
        $("#image_name").focus();
    });

    $('#action-dialog').on('show.bs.modal', function (event) {
        var button = $(event.relatedTarget);
        var id = button.data('value');
        var name = button.data('name');
        var dialog = $(this);
        utils.config_dialog(
            dialog,
            'Remove policy ' + name + "?",
            "Do you want to remove policy " + name + "? ",
            function() {
                resources.Policies.delete(id)
                   .done(function() { window.location.reload(); })
                    .fail(utils.ajax_error_dialog);
            }
        );
    });
});
<|endoftext|>


<commit_before>Application.Services.factory('User',
    ["$window", function ($window) {
        var self = this;
        if($window.sessionStorage.mcuser){
            self.mcuser = JSON.parse($window.sessionStorage.mcuser);
        }

        return {
            isAuthenticated: function () {
                return self.mcuser;
            },

            setAuthenticated: function (authenticated, u) {
                if (!authenticated) {
                    $window.sessionStorage.mcuser = null;
                    self.mcuser = undefined;
                } else {
                    var mcuser = {};
                    mcuser = u;
                    $window.sessionStorage.mcuser = JSON.stringify(mcuser);
                    self.mcuser = mcuser;
                }
            },

            apikey: function () {
                var key = self.mcuser ? self.mcuser.apikey : undefined;
                return key;
            },

            keyparam: function() {
                return {apikey: self.mcuser.apikey};
            },

            u: function () {
                return self.mcuser ? self.mcuser.email : 'Login';
            },

            attr: function() {
                return self.mcuser;
            },

            reset_apikey: function (new_key) {
                if (self.mcuser) {
                    self.mcuser.apikey = new_key;
                    $window.sessionStorage.mcuser = self.mcuser;
                }
            },
            save: function(user){
                if(self.mcuser){
                    self.mcuser.fullname = user.fullname;
                    $window.sessionStorage.mcuser = self.mcuser;
                }
            }

        };
    }]);
<commit_msg>Wrap parsing the JSON in a try/catch block. If the JSON is invalid then the user should be prompted to login.<commit_after>Application.Services.factory('User',
    ["$window", function ($window) {
        var self = this;
        if ($window.sessionStorage.mcuser) {
            try {
                self.mcuser = JSON.parse($window.sessionStorage.mcuser);
            } catch (err) {
                console.log("Error parse mcuser in sessionStorage");
                self.mcuser = null;
            }
        }

        return {
            isAuthenticated: function () {
                return self.mcuser;
            },

            setAuthenticated: function (authenticated, u) {
                if (!authenticated) {
                    $window.sessionStorage.mcuser = null;
                    self.mcuser = undefined;
                } else {
                    var mcuser = {};
                    mcuser = u;
                    $window.sessionStorage.mcuser = JSON.stringify(mcuser);
                    self.mcuser = mcuser;
                }
            },

            apikey: function () {
                var key = self.mcuser ? self.mcuser.apikey : undefined;
                return key;
            },

            keyparam: function() {
                return {apikey: self.mcuser.apikey};
            },

            u: function () {
                return self.mcuser ? self.mcuser.email : 'Login';
            },

            attr: function() {
                return self.mcuser;
            },

            reset_apikey: function (new_key) {
                if (self.mcuser) {
                    self.mcuser.apikey = new_key;
                    $window.sessionStorage.mcuser = self.mcuser;
                }
            },
            save: function(user){
                if(self.mcuser){
                    self.mcuser.fullname = user.fullname;
                    $window.sessionStorage.mcuser = self.mcuser;
                }
            }

        };
    }]);
<|endoftext|>


<commit_before>import React from 'react'
import 'spectrum-colorpicker'
import 'spectrum-colorpicker/spectrum.css'
import './index.less'
import 'react-widgets/lib/less/react-widgets.less'
import NumberPicker from 'react-widgets/lib/NumberPicker'
import DropdownList from 'react-widgets/lib/DropdownList'
import localizer from 'react-widgets/lib/localizers/simple-number'
import ColorStops from './colorStops'
import AngleInput from 'angle-input/react'
import 'angle-input/angle-input.css'
localizer()
const gradientExtentSelectionArr = [
  {value: 'closest-corner', text: 'closest-corner'},
  {value: 'closest-side', text: 'closest-side'},
  {value: 'farthest-corner', text: 'farthest-corner'},
  {value: 'farthest-side', text: 'farthest-side'},
  {value: 'length', text: 'length...'}
]
const linearGradientDirectionArr = [
  {value: 'to left', text: 'to left'},
  {value: 'to right', text: 'to right'},
  {value: 'to top', text: 'to top'},
  {value: 'to bottom', text: 'to bottom'},
  {value: 'to left top', text: 'to left top'},
  {value: 'to right top', text: 'to right top'},
  {value: 'to right bottom', text: 'to right bottom'},
  {value: 'to left bottom', text: 'to left bottom'},
  {value: 'to angle', text: 'to angle'}
]
export default React.createClass({
  componentDidMount() {
    $("#colorpicker").spectrum({
      color: this.props.currentStyle,
      showAlpha: true,
      showInput: true,
      allowEmpty: true,
      preferredFormat: 'rgb',
      change: (tinycolor) => {
        this.props.updateStyle({background: tinycolor && tinycolor.toRgbString()})
      },
    })
  },
  onToggleGradientShape: function () {
    let g = this.parseGradientString()
    if (g) {
      g.shape = arguments[0].target.value
      if (g.extent && g.extent.indexOf('px') >= 0) {
        g.extent = 'farthest-corner'
      }
    }
    let s = this.composeGradientString(g)
    this.props.updateStyle({background: s})
  },
  onToggleIsRepeating: function (evt) {
    let g = this.parseGradientString()
    if (!g) {
      return
    }
    g.isRepeating = evt.target.checked
    let s = this.composeGradientString(g)
    this.props.updateStyle({background: s})
  },
  onChangeGradientExtent: function () {
    let g = this.parseGradientString()
    if (g) {
      g.extent = arguments[0].value
      if (arguments[0].value === 'length') {
        g.extent = '100px'
        if (!g.shape || g.shape === 'ellipse') {
          g.extent += ' 100px'
        }
      }
    }
    let s = this.composeGradientString(g)
    this.props.updateStyle({background: s})
  },
  onChangeGradientExtentPct: function (dimension, value) {
    let g = this.parseGradientString()
    if (g && g.extent) {
      let xyExtArr = g.extent.split(' ')
      xyExtArr[dimension] = value + 'px'
      g.extent = xyExtArr.join(' ')
    }
    let s = this.composeGradientString(g)
    this.props.updateStyle({background: s})
  },
  onChangeGradientDirection: function () {
    let g = this.parseGradientString()
    if (g) {
      g.direction = arguments[0].value
      if (arguments[0].value === 'to angle') {
        g.direction = '0deg'
      }
    }
    let s = this.composeGradientString(g)
    this.props.updateStyle({background: s})
  },
  onChangeGradientDirectionAngle: function () {
    let g = this.parseGradientString()
    if (g) {
      g.direction = (-arguments[0] + 90 + 360) % 360 + 'deg'
    }
    let s = this.composeGradientString(g)
    this.props.updateStyle({background: s})
  },

  componentDidUpdate: function () {
    $("#colorpicker").spectrum("set", this.props.currentStyle)
  },
  parseGradientString: function () {
    let gradientString, gradientFormatMatch, gradientFormat = {}
    let gradientStringMatch = this.props.currentStyle.match(/(repeating-)?(linear|radial)-gradient\((.*)\)/)
    if (!gradientStringMatch) {
      return {}
    }
    gradientFormat.isRepeating = gradientStringMatch[1] !== undefined
    gradientFormat.type = gradientStringMatch[2]
    gradientString = gradientStringMatch[3]
    switch (gradientFormat.type) {
      case 'radial':
        gradientFormatMatch = gradientString.match(/(circle|ellipse)\s*(.*?)\s*(?:at\s*(\S*?)\s*(\S*?)\s*)?,/)
        if (gradientFormatMatch) {
          gradientFormat.shape = gradientFormatMatch[1]
          gradientFormat.extent = gradientFormatMatch[2]
          gradientFormat.position = {}
          gradientFormat.position.x = gradientFormatMatch[3]
          gradientFormat.position.y = gradientFormatMatch[4]
        }
        break
      case 'linear':
        gradientFormatMatch = gradientString.match(/(to.+?|.+?deg),/)
        if (gradientFormatMatch) {
          gradientFormat.direction = gradientFormatMatch[1]
          let gradientDegreeMatch = gradientFormat.direction.trim().match(/(.+)deg/)
          if (gradientDegreeMatch) {
            gradientFormat.directionAngle = gradientDegreeMatch[1]
          }
        }
        break
    }
    gradientFormat.colorStops = gradientString.match(/((?:rgba?.*?\)|#)[^,]*)/g)
    gradientFormat.colorStops = gradientFormat.colorStops && gradientFormat.colorStops.map(function (e, i, a) {
        let ret = {}
        let colorPosArr = e.trim().match(/(rgba?.*?\)|[\w#\.%]+)/g)
        let c = colorPosArr[0]
        let p
        if (colorPosArr.length > 1) {
          p = Number(colorPosArr[1].trim().replace('%', ''))
        }
        else {
          if (i == 0) {
            p = 0
          }
          else if (i === (a.length - 1)) {
            p = 100
          }
        }
        ret.c = c
        ret.p = p
        return ret
      })
    return gradientFormat
  },
  composeGradientString: function () {
    let gradientFormat, colorStops
    if (arguments[0].constructor === Array) {
      colorStops = arguments[0]
    }
    else {
      colorStops = arguments[0].colorStops
      gradientFormat = arguments[0]
    }
    let gradientFormatString = ''
    if (gradientFormat) {
      gradientFormatString = (gradientFormat.type === 'linear' && gradientFormat.direction ? gradientFormat.direction : '') + ' '
      gradientFormatString += (gradientFormat.type === 'radial' ? (gradientFormat.shape || 'ellipse') : '') + ' '
      gradientFormatString += (gradientFormat.extent || '') + ' '
      gradientFormatString += (gradientFormat.position && (gradientFormat.position.x !== undefined || gradientFormat.position.y !== undefined)) ? ' at ' : ''
      gradientFormatString += ((gradientFormat.position && gradientFormat.position.x !== undefined) ? gradientFormat.position.x : '') + ' '
      gradientFormatString += ((gradientFormat.position && gradientFormat.position.y !== undefined) ? gradientFormat.position.y : '') + ' '
      gradientFormatString = gradientFormatString.replace(/\s+/g, ' ').trim()
      gradientFormatString += gradientFormatString.length > 0 ? ',' : ''
    }
    let gradientStringArr = colorStops && colorStops.sort(function (a, b) {
        return a.p - b.p
      }).map(function (e) {
        return e.c + ' ' + e.p + '%'
      })
    let gradientString = gradientStringArr && gradientStringArr.join(', ')
    let fullGradientString = this.props.currentStyle.replace(/-gradient\(.*\)/, '-gradient(' + gradientFormatString + gradientString + ')')
    if (gradientFormat) {
      fullGradientString = fullGradientString.replace(/(repeating-)?(linear|radial)-gradient/, (match, p1, p2)=>
        ((gradientFormat.isRepeating ? 'repeating-' : '') + gradientFormat.type + '-gradient')
      )
    }
    return fullGradientString
  },
  render: function () {
    let type, gradientExtentSelect, gradientExtentXExtentPct, gradientExtentYExtentPct, gradientDirection, gradientAngle, gradientFormat
    if (this.props.currentStyle) {
      gradientFormat = this.parseGradientString()
      if (gradientFormat) {
        gradientExtentSelect = gradientFormat.extent || 'farthest-corner'
        if (gradientExtentSelect && gradientExtentSelect.indexOf('px') >= 0) {
          let xyExtArr = gradientExtentSelect.split(' ')
          gradientExtentXExtentPct = parseInt(xyExtArr[0])
          if (xyExtArr.length > 0) {
            gradientExtentYExtentPct = parseInt(xyExtArr[1])
          }
          gradientExtentSelect = 'length'
        }
        gradientDirection = gradientFormat.direction
        if (gradientDirection && gradientDirection.indexOf('deg') >= 0) {
          gradientDirection = 'to angle'
          gradientAngle = gradientFormat.directionAngle
        }
      }
      if (this.props.currentStyle.match(/^radial-gradient/)) {
        type = 'radial-gradient'
      }
      else if (this.props.currentStyle.match(/^linear-gradient/)) {
        type = 'linear-gradient'
      }
      else if (this.props.currentStyle.match(/^(rgba?\(|transparent)/)) {
        type = 'color'
      }
    }
    return

 {
            this.props.updateStyle({background: 'rgb()'})
            return true
          }}>

              {type === 'color' ? "radio_button_checked" : "radio_button_unchecked"}
                color

              <input id='colorpicker'/>

 {
                 this.props.updateStyle({background: 'linear-gradient()'})
               }}>

              {type === 'linear-gradient' ? "radio_button_checked" : "radio_button_unchecked"}
              linear gradient

Repeating:

                  <input type="checkbox" name="isRepeating"
                         onClick={this.onToggleIsRepeating}
                         defaultChecked={gradientFormat && gradientFormat.isRepeating}/>

Direction:

                <DropdownList className="col-xs-4" data={linearGradientDirectionArr}
                              valueField="value" textField="text"
                              value={gradientDirection}
                              onChange={this.onChangeGradientDirection}
                />

                  <AngleInput
                    className="col-xs-1 default-input angle-input noselect"
                    onInput={this.onChangeGradientDirectionAngle}
                    onChange={this.onChangeGradientDirectionAngle}
                    defaultValue={(-parseInt(gradientAngle || 0) + 90 + 360) % 360}
                  >
                    {gradientAngle}°
                  </AngleInput>

              Color Stops:
              <ColorStops id="sp-linear-color-stops"
                          parseGradientString={this.parseGradientString}
                          updateStyle={this.props.updateStyle}
                          composeGradientString={this.composeGradientString}
              ></ColorStops>

 {
                 this.props.updateStyle({background: 'radial-gradient()'})
               }}>

              {type === 'radial-gradient' ? "radio_button_checked" : "radio_button_unchecked"}
              radial gradient

Repeating:

                  <input type="checkbox" name="isRepeating"
                         onClick={this.onToggleIsRepeating}
                         defaultChecked={gradientFormat && gradientFormat.isRepeating}/>

Shape:

                  <input type="radio" name="shape" value="circle"
                         onClick={this.onToggleGradientShape}
                         defaultChecked={gradientFormat && gradientFormat.shape === 'circle'}/>Circle

                  <input type="radio" name="shape" value="ellipse"
                         onClick={this.onToggleGradientShape}
                         defaultChecked={gradientFormat && gradientFormat.shape === 'ellipse'}/>Ellipse

Extent:

                <DropdownList data={gradientExtentSelectionArr}
                              valueField="value" textField="text"
                              value={gradientExtentSelect}
                              onChange={this.onChangeGradientExtent}
                              className="col-xs-4"
                />

                  <NumberPicker value={gradientExtentXExtentPct} min={0}
                                onChange={this.onChangeGradientExtentPct.bind(null, 0)}/>px

                  x: <NumberPicker value={gradientExtentXExtentPct} min={0}
                                   onChange={this.onChangeGradientExtentPct.bind(null, 0)}/>px
                y: <NumberPicker value={gradientExtentYExtentPct} min={0}
                                 onChange={this.onChangeGradientExtentPct.bind(null, 1)}/>px

              Color Stops:
              <ColorStops id="sp-radial-color-stops"
                          parseGradientString={this.parseGradientString}
                          updateStyle={this.props.updateStyle}
                          composeGradientString={this.composeGradientString}
              ></ColorStops>

  }
})
<commit_msg>cleanup<commit_after>import React from 'react'
import 'spectrum-colorpicker'
import 'spectrum-colorpicker/spectrum.css'
import './index.less'
import 'react-widgets/lib/less/react-widgets.less'
import NumberPicker from 'react-widgets/lib/NumberPicker'
import DropdownList from 'react-widgets/lib/DropdownList'
import localizer from 'react-widgets/lib/localizers/simple-number'
import ColorStops from './colorStops'
import AngleInput from 'angle-input/react'
import 'angle-input/angle-input.css'
localizer()
const gradientExtentSelectionArr = [
  {value: 'closest-corner', text: 'closest-corner'},
  {value: 'closest-side', text: 'closest-side'},
  {value: 'farthest-corner', text: 'farthest-corner'},
  {value: 'farthest-side', text: 'farthest-side'},
  {value: 'length', text: 'length...'}
]
const linearGradientDirectionArr = [
  {value: 'to left', text: 'to left'},
  {value: 'to right', text: 'to right'},
  {value: 'to top', text: 'to top'},
  {value: 'to bottom', text: 'to bottom'},
  {value: 'to left top', text: 'to left top'},
  {value: 'to right top', text: 'to right top'},
  {value: 'to right bottom', text: 'to right bottom'},
  {value: 'to left bottom', text: 'to left bottom'},
  {value: 'to angle', text: 'to angle'}
]
export default React.createClass({
  componentDidMount() {
    $("#colorpicker").spectrum({
      color: this.props.currentStyle,
      showAlpha: true,
      showInput: true,
      allowEmpty: true,
      preferredFormat: 'rgb',
      change: (tinycolor) => {
        this.props.updateStyle({background: tinycolor && tinycolor.toRgbString()})
      },
    })
  },
  onToggleGradientShape: function () {
    let g = this.parseGradientString()
    if (g) {
      g.shape = arguments[0].target.value
      if (g.extent && g.extent.indexOf('px') >= 0) {
        g.extent = 'farthest-corner'
      }
    }
    let s = this.composeGradientString(g)
    this.props.updateStyle({background: s})
  },
  onToggleIsRepeating: function (evt) {
    let g = this.parseGradientString()
    if (!g) {
      return
    }
    g.isRepeating = evt.target.checked
    let s = this.composeGradientString(g)
    this.props.updateStyle({background: s})
  },
  onChangeGradientExtent: function () {
    let g = this.parseGradientString()
    if (g) {
      g.extent = arguments[0].value
      if (arguments[0].value === 'length') {
        g.extent = '100px'
        if (!g.shape || g.shape === 'ellipse') {
          g.extent += ' 100px'
        }
      }
    }
    let s = this.composeGradientString(g)
    this.props.updateStyle({background: s})
  },
  onChangeGradientExtentPct: function (dimension, value) {
    let g = this.parseGradientString()
    if (g && g.extent) {
      let xyExtArr = g.extent.split(' ')
      xyExtArr[dimension] = value + 'px'
      g.extent = xyExtArr.join(' ')
    }
    let s = this.composeGradientString(g)
    this.props.updateStyle({background: s})
  },
  onChangeGradientDirection: function () {
    let g = this.parseGradientString()
    if (g) {
      g.direction = arguments[0].value
      if (arguments[0].value === 'to angle') {
        g.direction = '0deg'
      }
    }
    let s = this.composeGradientString(g)
    this.props.updateStyle({background: s})
  },
  onChangeGradientDirectionAngle: function () {
    let g = this.parseGradientString()
    if (g) {
      g.direction = (-arguments[0] + 90 + 360) % 360 + 'deg'
    }
    let s = this.composeGradientString(g)
    this.props.updateStyle({background: s})
  },

  componentDidUpdate: function () {
    $("#colorpicker").spectrum("set", this.props.currentStyle)
  },
  parseGradientString: function () {
    let gradientString, gradientFormatMatch, gradientFormat = {}
    let gradientStringMatch = this.props.currentStyle.match(/(repeating-)?(linear|radial)-gradient\((.*)\)/)
    if (!gradientStringMatch) {
      return {}
    }
    gradientFormat.isRepeating = gradientStringMatch[1] !== undefined
    gradientFormat.type = gradientStringMatch[2]
    gradientString = gradientStringMatch[3]
    switch (gradientFormat.type) {
      case 'radial':
        gradientFormatMatch = gradientString.match(/(circle|ellipse)\s*(.*?)\s*(?:at\s*(\S*?)\s*(\S*?)\s*)?,/)
        if (gradientFormatMatch) {
          gradientFormat.shape = gradientFormatMatch[1]
          gradientFormat.extent = gradientFormatMatch[2]
          gradientFormat.position = {}
          gradientFormat.position.x = gradientFormatMatch[3]
          gradientFormat.position.y = gradientFormatMatch[4]
        }
        break
      case 'linear':
        gradientFormatMatch = gradientString.match(/(to.+?|.+?deg),/)
        if (gradientFormatMatch) {
          gradientFormat.direction = gradientFormatMatch[1]
          let gradientDegreeMatch = gradientFormat.direction.trim().match(/(.+)deg/)
          if (gradientDegreeMatch) {
            gradientFormat.directionAngle = gradientDegreeMatch[1]
          }
        }
        break
    }
    gradientFormat.colorStops = gradientString.match(/((?:rgba?.*?\)|#)[^,]*)/g)
    gradientFormat.colorStops = gradientFormat.colorStops && gradientFormat.colorStops.map(function (e, i, a) {
        let ret = {}
        let colorPosArr = e.trim().match(/(rgba?.*?\)|[\w#\.%]+)/g)
        let c = colorPosArr[0]
        let p
        if (colorPosArr.length > 1) {
          p = Number(colorPosArr[1].trim().replace('%', ''))
        }
        else {
          if (i == 0) {
            p = 0
          }
          else if (i === (a.length - 1)) {
            p = 100
          }
        }
        ret.c = c
        ret.p = p
        return ret
      })
    return gradientFormat
  },
  composeGradientString: function () {
    let gradientFormat, colorStops
    if (arguments[0].constructor === Array) {
      colorStops = arguments[0]
    }
    else {
      colorStops = arguments[0].colorStops
      gradientFormat = arguments[0]
    }
    let gradientFormatString = ''
    if (gradientFormat) {
      gradientFormatString = (gradientFormat.type === 'linear' && gradientFormat.direction ? gradientFormat.direction : '') + ' '
      gradientFormatString += (gradientFormat.type === 'radial' ? (gradientFormat.shape || 'ellipse') : '') + ' '
      gradientFormatString += (gradientFormat.extent || '') + ' '
      gradientFormatString += (gradientFormat.position && (gradientFormat.position.x !== undefined || gradientFormat.position.y !== undefined)) ? ' at ' : ''
      gradientFormatString += ((gradientFormat.position && gradientFormat.position.x !== undefined) ? gradientFormat.position.x : '') + ' '
      gradientFormatString += ((gradientFormat.position && gradientFormat.position.y !== undefined) ? gradientFormat.position.y : '') + ' '
      gradientFormatString = gradientFormatString.replace(/\s+/g, ' ').trim()
      gradientFormatString += gradientFormatString.length > 0 ? ',' : ''
    }
    let gradientStringArr = colorStops && colorStops.sort(function (a, b) {
        return a.p - b.p
      }).map(function (e) {
        return e.c + ' ' + e.p + '%'
      })
    let gradientString = gradientStringArr ? gradientStringArr.join(', ') : ''
    let fullGradientString = this.props.currentStyle.replace(/-gradient\(.*\)/, '-gradient(' + gradientFormatString + gradientString + ')')
    if (gradientFormat) {
      fullGradientString = fullGradientString.replace(/(repeating-)?(linear|radial)-gradient/, (match, p1, p2)=>
        ((gradientFormat.isRepeating ? 'repeating-' : '') + gradientFormat.type + '-gradient')
      )
    }
    return fullGradientString
  },
  render: function () {
    let type, gradientExtentSelect, gradientExtentXExtentPct, gradientExtentYExtentPct, gradientDirection, gradientAngle, gradientFormat
    if (this.props.currentStyle) {
      gradientFormat = this.parseGradientString()
      if (gradientFormat) {
        gradientExtentSelect = gradientFormat.extent || 'farthest-corner'
        if (gradientExtentSelect && gradientExtentSelect.indexOf('px') >= 0) {
          let xyExtArr = gradientExtentSelect.split(' ')
          gradientExtentXExtentPct = parseInt(xyExtArr[0])
          if (xyExtArr.length > 0) {
            gradientExtentYExtentPct = parseInt(xyExtArr[1])
          }
          gradientExtentSelect = 'length'
        }
        gradientDirection = gradientFormat.direction
        if (gradientDirection && gradientDirection.indexOf('deg') >= 0) {
          gradientDirection = 'to angle'
          gradientAngle = gradientFormat.directionAngle
        }
      }
      if (this.props.currentStyle.match(/^(repeating-)?radial-gradient/)) {
        type = 'radial-gradient'
      }
      else if (this.props.currentStyle.match(/^(repeating-)?linear-gradient/)) {
        type = 'linear-gradient'
      }
      else if (this.props.currentStyle.match(/^(rgba?\(|transparent)/)) {
        type = 'color'
      }
    }
    return

 {
            this.props.updateStyle({background: 'rgb()'})
            return true
          }}>

              {type === 'color' ? "radio_button_checked" : "radio_button_unchecked"}
                color

              <input id='colorpicker'/>

 {
                 this.props.updateStyle({background: 'linear-gradient()'})
               }}>

              {type === 'linear-gradient' ? "radio_button_checked" : "radio_button_unchecked"}
              linear gradient

Repeating:

                  <input type="checkbox"
                         onClick={this.onToggleIsRepeating}
                         defaultChecked={gradientFormat && gradientFormat.isRepeating}/>

Direction:

                <DropdownList className="col-xs-4" data={linearGradientDirectionArr}
                              valueField="value" textField="text"
                              value={gradientDirection}
                              onChange={this.onChangeGradientDirection}
                />

                  <AngleInput
                    className="col-xs-1 default-input angle-input noselect"
                    onInput={this.onChangeGradientDirectionAngle}
                    onChange={this.onChangeGradientDirectionAngle}
                    defaultValue={(-parseInt(gradientAngle || 0) + 90 + 360) % 360}
                  >
                    {gradientAngle}°
                  </AngleInput>

              Color Stops:
              <ColorStops id="sp-linear-color-stops"
                          parseGradientString={this.parseGradientString}
                          updateStyle={this.props.updateStyle}
                          composeGradientString={this.composeGradientString}
              ></ColorStops>

 {
                 this.props.updateStyle({background: 'radial-gradient()'})
               }}>

              {type === 'radial-gradient' ? "radio_button_checked" : "radio_button_unchecked"}
              radial gradient

Repeating:

                  <input type="checkbox"
                         onClick={this.onToggleIsRepeating}
                         defaultChecked={gradientFormat && gradientFormat.isRepeating}/>

Shape:

                  <input type="radio" name="shape" value="circle"
                         onClick={this.onToggleGradientShape}
                         defaultChecked={gradientFormat && gradientFormat.shape === 'circle'}/>Circle

                  <input type="radio" name="shape" value="ellipse"
                         onClick={this.onToggleGradientShape}
                         defaultChecked={gradientFormat && gradientFormat.shape === 'ellipse'}/>Ellipse

Extent:

                <DropdownList data={gradientExtentSelectionArr}
                              valueField="value" textField="text"
                              value={gradientExtentSelect}
                              onChange={this.onChangeGradientExtent}
                              className="col-xs-4"
                />

                  <NumberPicker value={gradientExtentXExtentPct} min={0}
                                onChange={this.onChangeGradientExtentPct.bind(null, 0)}/>px

                  x: <NumberPicker value={gradientExtentXExtentPct} min={0}
                                   onChange={this.onChangeGradientExtentPct.bind(null, 0)}/>px
                y: <NumberPicker value={gradientExtentYExtentPct} min={0}
                                 onChange={this.onChangeGradientExtentPct.bind(null, 1)}/>px

              Color Stops:
              <ColorStops id="sp-radial-color-stops"
                          parseGradientString={this.parseGradientString}
                          updateStyle={this.props.updateStyle}
                          composeGradientString={this.composeGradientString}
              ></ColorStops>

  }
})
<|endoftext|>


<commit_before>/*
Copyright 2016 Brazil Ltd.

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.
*/
const path = require('path');
const awaiter = require('./awaiter.js');
const fs = require('fs');
const df = require('./df.js');

function expire(options) {
  return new Promise((resolve, reject) => {
    awaiter(function* exec(sleep) {
      let recorded;
      try {
        recorded = JSON.parse(fs.readFileSync(options.recordedFile));
      } catch (e) {
        console.error('%s: %s', new Date().toISOString(), e.message);
        return;
      }
      yield sleep(100);
      const expirableRecords = recorded.filter(a => a.autoExpire);
      console.log('%s: Found %d expirable records.', new Date().toISOString(), expirableRecords.length);
      yield sleep(100);
      expirableRecords.sort((a, b) => a.end - b.end);
      yield sleep(100);
      const expiredRecords = [];
      while (
        expirableRecords.length > 0 &&
        df(options.recordedDir).free < options.extraDiskSpace
      ) {
        const record = expirableRecords.shift();
        try {
          fs.unlinkSync(path.resolve(options.chinachuDir, record.recorded));
          console.log(
            '%s: %s "%s" deleted.',
            new Date().toISOString(),
            record.id,
            path.basename(record.recorded)
          );
          expiredRecords.push(record.id);
        } catch (e) {
          console.error('%s: %s', new Date().toISOString(), e.message);
        }
        yield sleep(100);
      }
      recorded = recorded.filter(a => expiredRecords.indexOf(a.id) < 0);
      yield sleep(100);
      try {
        fs.writeFileSync(options.recordedFile, JSON.stringify(recorded));
        resolve();
      } catch (e) {
        console.error('%s: %s', new Date().toISOString(), e.message);
        reject(e);
      }
      console.log('%s: %d records expired.', new Date().toISOString(), expiredRecords.length);
    }, e => reject(e));
  });
}

module.exports = expire;
<commit_msg>miyoutv-agent: 自動録画の削除処理を修正<commit_after>/*
Copyright 2016 Brazil Ltd.

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.
*/
const path = require('path');
const awaiter = require('./awaiter.js');
const fs = require('fs');
const df = require('./df.js');

function expire(options) {
  return new Promise((resolve, reject) => {
    awaiter(function* exec(sleep) {
      let recorded;
      try {
        recorded = JSON.parse(fs.readFileSync(options.recordedFile));
      } catch (e) {
        console.error('%s: %s', new Date().toISOString(), e.message);
        reject(e);
        return;
      }
      yield sleep(100);
      const expirableRecords = recorded.filter(a => a.autoExpire);
      console.log('%s: Found %d expirable records.', new Date().toISOString(), expirableRecords.length);
      yield sleep(100);
      expirableRecords.sort((a, b) => a.end - b.end);
      yield sleep(100);
      const expiredRecords = [];
      while (
        expirableRecords.length > 0 &&
        df(options.recordedDir).free < options.extraDiskSpace
      ) {
        const record = expirableRecords.shift();
        try {
          fs.unlinkSync(path.resolve(options.chinachuDir, record.recorded));
          console.log(
            '%s: %s "%s" deleted.',
            new Date().toISOString(),
            record.id,
            path.basename(record.recorded)
          );
          expiredRecords.push(record.id);
        } catch (e) {
          console.error('%s: %s', new Date().toISOString(), e.message);
          if (e.code === 'ENOENT') {
            expiredRecords.push(record.id);
          }
        }
        yield sleep(100);
      }
      recorded = recorded.filter(a => expiredRecords.indexOf(a.id) < 0);
      yield sleep(100);
      try {
        fs.writeFileSync(options.recordedFile, JSON.stringify(recorded));
        resolve();
      } catch (e) {
        console.error('%s: %s', new Date().toISOString(), e.message);
        reject(e);
      }
      console.log('%s: %d records expired.', new Date().toISOString(), expiredRecords.length);
    }, e => reject(e));
  });
}

module.exports = expire;
<|endoftext|>


<commit_before>let Item = require('../Item');
let List = require('../List');
let Tree = require('../Tree');

describe('Tree', function () {

  describe('#constructor', function () {

    it('defaults to an empty array', function () {
      let tree = new Tree();
      expect(tree.getItems()).toEqual([]);
    });

    it('accepts a list of items', function () {
      let tree = new Tree({items: [0, 1, 2]});
      expect(tree.getItems()).toEqual([0, 1, 2]);
    });

    it('accepts a nested items', function () {
      let tree = new Tree({items: [0, {items: [1.1, 1.2, 1.2]}, 2]});
      expect(tree.getItems()[1] instanceof Tree).toEqual(true);
    });

    it('throws when initialized with a non-array argument', function () {
      let fn = function () {
        return new Tree({items: 'foo'});
      };

      expect(fn).toThrow();
    });

  });

  describe('#add', function () {

    it('adds an item', function () {
      let tree = new Tree();
      tree.add(0);
      expect(tree.getItems()).toEqual([0]);
    });

    it('adds two items', function () {
      let tree = new Tree();
      tree.add(0);
      tree.add(1);
      expect(tree.getItems()).toEqual([0, 1]);
    });

    it('adds items to current Tree', function () {
      let tree = new Tree({items: [0]});
      tree.add(1);
      tree.add(2);
      expect(tree.getItems()).toEqual([0, 1, 2]);
    });

  });

  describe('#getItems', function () {

    it('returns a list of items', function () {
      let tree = new Tree();
      expect(tree.getItems()).toEqual([]);
    });

    it('returns added items in a list', function () {
      let tree = new Tree();
      tree.add(0);
      tree.add(1);
      expect(tree.getItems()).toEqual([0, 1]);
    });

  });

  describe('#last', function () {

    it('returns undefined when there\'s no last item', function () {
      let tree = new Tree();
      expect(tree.last()).toEqual(undefined);
    });

    it('returns the last item in the list', function () {
      let tree = new Tree({items: [0, 1, 2, 3]});
      expect(tree.last()).toEqual(3);
    });

  });

  describe('#flattenItems', function () {

    beforeEach(function () {
      this.instance = new Tree({items: [
        {name: 'foo'}, {name: 'bar'}, {items: [{name: 'alpha'}, {name: 'beta'}]}
      ]});
    });

    it('returns an instance of List', function () {
      expect(this.instance.flattenItems() instanceof List).toBeTruthy();
    });

    it('returns correct list of items', function () {
      expect(this.instance.flattenItems().getItems()).toEqual([
        {name: 'foo'},
        {name: 'bar'},
        {name: 'alpha'},
        {name: 'beta'}
      ]);
    });

  });

  describe('#filterItems', function () {

    beforeEach(function () {
      var items = [
        {name: 'foo', description: {value: 'qux'}, subItems: ['one', 'two']},
        {name: 'bar', description: {value: 'quux'}, subItems: ['two', 'three']},
        {
          items: [
            {
              name: 'alpha',
              description: {value: 'gamma'},
              subItems: ['one', 'two']
            },
            {
              name: 'beta',
              description: {value: 'delta'},
              subItems: ['one', 'two']
            }
          ]
        }
      ];
      var filterProperties = {
        name: null,
        description: function (item, prop) {
          return item[prop] && item[prop].value;
        },
        subItems: function (item, prop) {
          return item[prop] && item[prop].join(' ');
        }
      };

      this.instance = new Tree({items, filterProperties});
    });

    it('should return an instance of Tree', function () {
      let filteredTree = this.instance.filterItems('bar');
      expect(filteredTree instanceof Tree).toEqual(true);
    });

    it('should filter sub trees', function () {
      let filteredSubtree = this.instance.filterItems('alpha').getItems()[0];
      expect(filteredSubtree instanceof Tree).toEqual(true);
      expect(filteredSubtree.getItems()[0].name).toEqual('alpha');
    });

    it('should filter instances of Item', function () {
      var items = [
        new Item({
          name: 'foo',
          description: {value: 'qux'},
          subItems: ['one', 'two']
        }),
        new Item({
          name: 'bar',
          description: {value: 'quux'},
          subItems: ['two', 'three']}
        )
      ];
      var filterProperties = {
        name: null,
        description: function (item, prop) {
          return item[prop] && item[prop].value;
        },
        subItems: function (item, prop) {
          return item[prop] && item[prop].join(' ');
        }
      };

      this.instance = new Tree({items, filterProperties});
      var filteredItems = this.instance.filterItems('bar').getItems();
      expect(filteredItems.length).toEqual(1);
      expect(filteredItems[0] instanceof Item).toEqual(true);
    });

    it('should filter by default getter', function () {
      var filteredItems = this.instance.filterItems('bar').getItems();
      expect(filteredItems.length).toEqual(1);
      expect(filteredItems[0].name).toEqual('bar');
    });

    it('should filter by description', function () {
      var filteredItems = this.instance.filterItems('qux').getItems();
      expect(filteredItems.length).toEqual(1);
      expect(filteredItems[0].description.value).toEqual('qux');
    });

    it('should filter by subItems', function () {
      var filteredItems = this.instance.filterItems('two').getItems();
      expect(filteredItems.length).toEqual(3);
      expect(filteredItems[0].subItems).toEqual(['one', 'two']);
      expect(filteredItems[1].subItems).toEqual(['two', 'three']);
    });

    it('should handle filter by with null elements', function () {
      var items = [
        {name: null, description: {value: null}, subItems: [null, 'three']},
        {description: null, subItems: null}
      ];
      var filterProperties = {
        name: null,
        description: function (item, prop) {
          return item[prop] && item[prop].value;
        },
        subItems: function (item, prop) {
          return item[prop] && item[prop].join(' ');
        }
      };
      var list = new Tree({items, filterProperties});
      expect(list.filterItems.bind(list, 'foo')).not.toThrow();
    });

  });

  describe('#findItem', function () {

    beforeEach(function () {
      this.instance = new Tree({items: [
        {name: 'foo'}, {name: 'bar'}, {items: [{name: 'alpha'}, {name: 'beta'}]}
      ]});
    });

    it('should return undefined if no matching item was found', function () {
      expect(this.instance.findItem(function () {
        return false;
      })).toEqual(undefined);
    });

    it('should return matchin item', function () {
      expect(this.instance.findItem(function (item) {
        return item.name === 'beta';
      })).toEqual({name: 'beta'});
    });

  });

});
<commit_msg>Remove unnecessary test<commit_after>let Item = require('../Item');
let List = require('../List');
let Tree = require('../Tree');

describe('Tree', function () {

  describe('#constructor', function () {

    it('defaults to an empty array', function () {
      let tree = new Tree();
      expect(tree.getItems()).toEqual([]);
    });

    it('accepts a list of items', function () {
      let tree = new Tree({items: [0, 1, 2]});
      expect(tree.getItems()).toEqual([0, 1, 2]);
    });

    it('accepts a nested items', function () {
      let tree = new Tree({items: [0, {items: [1.1, 1.2, 1.2]}, 2]});
      expect(tree.getItems()[1] instanceof Tree).toEqual(true);
    });

    it('throws when initialized with a non-array argument', function () {
      let fn = function () {
        return new Tree({items: 'foo'});
      };

      expect(fn).toThrow();
    });

  });

  describe('#add', function () {

    it('adds an item', function () {
      let tree = new Tree();
      tree.add(0);
      expect(tree.getItems()).toEqual([0]);
    });

    it('adds two items', function () {
      let tree = new Tree();
      tree.add(0);
      tree.add(1);
      expect(tree.getItems()).toEqual([0, 1]);
    });

    it('adds items to current Tree', function () {
      let tree = new Tree({items: [0]});
      tree.add(1);
      tree.add(2);
      expect(tree.getItems()).toEqual([0, 1, 2]);
    });

  });

  describe('#getItems', function () {

    it('returns a list of items', function () {
      let tree = new Tree();
      expect(tree.getItems()).toEqual([]);
    });

    it('returns added items in a list', function () {
      let tree = new Tree();
      tree.add(0);
      tree.add(1);
      expect(tree.getItems()).toEqual([0, 1]);
    });

  });

  describe('#flattenItems', function () {

    beforeEach(function () {
      this.instance = new Tree({items: [
        {name: 'foo'}, {name: 'bar'}, {items: [{name: 'alpha'}, {name: 'beta'}]}
      ]});
    });

    it('returns an instance of List', function () {
      expect(this.instance.flattenItems() instanceof List).toBeTruthy();
    });

    it('returns correct list of items', function () {
      expect(this.instance.flattenItems().getItems()).toEqual([
        {name: 'foo'},
        {name: 'bar'},
        {name: 'alpha'},
        {name: 'beta'}
      ]);
    });

  });

  describe('#filterItems', function () {

    beforeEach(function () {
      var items = [
        {name: 'foo', description: {value: 'qux'}, subItems: ['one', 'two']},
        {name: 'bar', description: {value: 'quux'}, subItems: ['two', 'three']},
        {
          items: [
            {
              name: 'alpha',
              description: {value: 'gamma'},
              subItems: ['one', 'two']
            },
            {
              name: 'beta',
              description: {value: 'delta'},
              subItems: ['one', 'two']
            }
          ]
        }
      ];
      var filterProperties = {
        name: null,
        description: function (item, prop) {
          return item[prop] && item[prop].value;
        },
        subItems: function (item, prop) {
          return item[prop] && item[prop].join(' ');
        }
      };

      this.instance = new Tree({items, filterProperties});
    });

    it('should return an instance of Tree', function () {
      let filteredTree = this.instance.filterItems('bar');
      expect(filteredTree instanceof Tree).toEqual(true);
    });

    it('should filter sub trees', function () {
      let filteredSubtree = this.instance.filterItems('alpha').getItems()[0];
      expect(filteredSubtree instanceof Tree).toEqual(true);
      expect(filteredSubtree.getItems()[0].name).toEqual('alpha');
    });

    it('should filter instances of Item', function () {
      var items = [
        new Item({
          name: 'foo',
          description: {value: 'qux'},
          subItems: ['one', 'two']
        }),
        new Item({
          name: 'bar',
          description: {value: 'quux'},
          subItems: ['two', 'three']}
        )
      ];
      var filterProperties = {
        name: null,
        description: function (item, prop) {
          return item[prop] && item[prop].value;
        },
        subItems: function (item, prop) {
          return item[prop] && item[prop].join(' ');
        }
      };

      this.instance = new Tree({items, filterProperties});
      var filteredItems = this.instance.filterItems('bar').getItems();
      expect(filteredItems.length).toEqual(1);
      expect(filteredItems[0] instanceof Item).toEqual(true);
    });

    it('should filter by default getter', function () {
      var filteredItems = this.instance.filterItems('bar').getItems();
      expect(filteredItems.length).toEqual(1);
      expect(filteredItems[0].name).toEqual('bar');
    });

    it('should filter by description', function () {
      var filteredItems = this.instance.filterItems('qux').getItems();
      expect(filteredItems.length).toEqual(1);
      expect(filteredItems[0].description.value).toEqual('qux');
    });

    it('should filter by subItems', function () {
      var filteredItems = this.instance.filterItems('two').getItems();
      expect(filteredItems.length).toEqual(3);
      expect(filteredItems[0].subItems).toEqual(['one', 'two']);
      expect(filteredItems[1].subItems).toEqual(['two', 'three']);
    });

    it('should handle filter by with null elements', function () {
      var items = [
        {name: null, description: {value: null}, subItems: [null, 'three']},
        {description: null, subItems: null}
      ];
      var filterProperties = {
        name: null,
        description: function (item, prop) {
          return item[prop] && item[prop].value;
        },
        subItems: function (item, prop) {
          return item[prop] && item[prop].join(' ');
        }
      };
      var list = new Tree({items, filterProperties});
      expect(list.filterItems.bind(list, 'foo')).not.toThrow();
    });

  });

  describe('#findItem', function () {

    beforeEach(function () {
      this.instance = new Tree({items: [
        {name: 'foo'}, {name: 'bar'}, {items: [{name: 'alpha'}, {name: 'beta'}]}
      ]});
    });

    it('should return undefined if no matching item was found', function () {
      expect(this.instance.findItem(function () {
        return false;
      })).toEqual(undefined);
    });

    it('should return matchin item', function () {
      expect(this.instance.findItem(function (item) {
        return item.name === 'beta';
      })).toEqual({name: 'beta'});
    });

  });

});
<|endoftext|>