<commit_before>import moment from 'moment';
import { isPresent } from "@ember/utils";
import { getWithDefault } from '@ember/object';
import { buildDateEod } from 'thirdeye-frontend/utils/utils';
import { getFormatedDuration } from 'thirdeye-frontend/utils/anomaly';
import floatToPercent from 'thirdeye-frontend/utils/float-to-percent';

/**
 * Handles types and defaults returned from eval/projected endpoints
 * @param {Number|String} metric - number or string like 'NaN', 'Infinity'
 * @param {Boolean} isPercentage - shall we treat this as a % or a whole number?
 * @returns {Object}
 */
export function formatEvalMetric(metric, isPercentage = false) {
  const isWhole = Number.isInteger(Number(metric));
  let shown = (metric === 'Infinity') ? metric : 'N/A';
  const multiplier = isPercentage ? 100 : 1;
  const convertedNum = metric * multiplier;
  const formattedNum = isWhole ? convertedNum : convertedNum.toFixed(1);
  let displayNum = isFinite(metric) ? formattedNum : shown;
  // Prevent meaninglessly large numbers
  return (Number(displayNum) > 10000) ? 'N/A' : displayNum;
}

/**
 * Pluralizes and formats the anomaly range duration string
 * @param {Number} time
 * @param {String} unit
 * @returns {String}
 */
export function pluralizeTime(time, unit) {
  const unitStr = time > 1 ? unit + 's' : unit;
  return time ? time + ' ' + unitStr : '';
}

/**
 * Split array of all anomalyIds into buckets of bucketSize
 * @param {Array} anomalyIds - array of anomaly Ids
 * @param {Number} bucketSize - number of anomalies per group
 * @returns {Array}
 */
export function toIdGroups(anomalyIds, bucketSize = 10) {
  const idGroups = anomalyIds.map((item, index) => {
    return (index % bucketSize === 0) ? anomalyIds.slice(index, index + bucketSize) : null;
  }).filter(item => item);
  return idGroups;
}

/**
 * Derives and formats extra anomaly properties such as duration
 * @param {Array} anomalies - array of raw anomalies
 * @returns {Array}
 */
export function enhanceAnomalies(rawAnomalies, severityScores) {
  const newAnomalies = [];
  const anomaliesPresent = rawAnomalies && rawAnomalies.length;
  // De-dupe raw anomalies, extract only the good stuff (anomalyDetailsList)
  const anomalies = anomaliesPresent ? [].concat(...rawAnomalies.map(data => data.anomalyDetailsList)) : [];
  // Extract all resolved scores from the RSVP promise response
  const resolvedScores = severityScores ? severityScores.map((score) => {
    return (score.state === 'fulfilled') ? score.value : '';
  }) : [];

  // Loop over all anomalies to configure display settings
  anomalies.forEach((anomaly) => {
    let dimensionList = [];
    let targetAnomaly = resolvedScores.find(score => Number(score.id) === Number(anomaly.anomalyId));
    // Extract current anomaly's score from array of all scores
    const score = resolvedScores.length && targetAnomaly ? targetAnomaly.score : null;
    // Set up anomaly change rate display
    const changeRate = (anomaly.current && anomaly.baseline) ? floatToPercent((anomaly.current - anomaly.baseline) / anomaly.baseline) : 0;
    const isNullChangeRate = Number.isNaN(Number(changeRate));
    // Set 'not reviewed' label
    if (!anomaly.anomalyFeedback) {
      anomaly.anomalyFeedback = 'Not reviewed yet';
    }
    // Add missing properties
    Object.assign(anomaly, {
      changeRate,
      isNullChangeRate,
      shownChangeRate: changeRate,
      isUserReported: anomaly.anomalyResultSource === 'USER_LABELED_ANOMALY',
      startDateStr: moment(anomaly.anomalyStart).format('MMM D, hh:mm A'),
      durationStr: getFormatedDuration(anomaly.anomalyStart, anomaly.anomalyEnd),
      severityScore: score && !isNaN(score) ? score.toFixed(2) : 'N/A',
      shownCurrent: Number(anomaly.current) > 0 ? anomaly.current : 'N/A',
      shownBaseline: Number(anomaly.baseline) > 0 ? anomaly.baseline : 'N/A',
      showResponseSaved: false,
      showResponseFailed: false
    });
    // Create a list of all available dimensions for toggling. Also massage dimension property.
    if (anomaly.anomalyFunctionDimension) {
      let dimensionObj = JSON.parse(anomaly.anomalyFunctionDimension);
      let dimensionStrArr = [];
      for (let dimension of Object.keys(dimensionObj)) {
        let dimensionKey = dimension.dasherize();
        let dimensionVal = dimensionObj[dimension].join(',');
        dimensionList.push({ dimensionKey, dimensionVal });
        dimensionStrArr.push(`${dimensionKey}:${dimensionVal}`);
      }
      let dimensionString = dimensionStrArr.join(' & ');
      Object.assign(anomaly, { dimensionList, dimensionString });
    }
    newAnomalies.push(anomaly);
  });
  // List most recent anomalies first
  return newAnomalies.sortBy('anomalyStart').reverse();
}

/**
 * Generates time range options for selection in the self-serve UI
 * @example output
 * [{ name: "3 Months", value: "3m", start: Moment, isActive: true },
 *  { name: "Custom", value: "custom", start: null, isActive: false }]
 * @method setUpTimeRangeOptions
 * @param {Array} datesKeys - array of keys used to generate time ranges
 * @param {String} duration - the selected time span that is default
 * @return {Array}
 */
export function setUpTimeRangeOptions(datesKeys, duration) {
  const newRangeArr = [];

  const defaultCustomRange = {
    name: 'Custom',
    value: 'custom',
    start: null,
    isActive: !datesKeys.includes(duration)
  };

  const dateKeyMap = new Map([
   [ '1m', ['Last 30 Days', 1, 'month'] ],
   [ '3m', ['3 Months', 3, 'month'] ],
   [ '2w', ['Last 2 Weeks', 2, 'week'] ],
   [ '1w', ['Last Week', 1, 'week'] ],
   [ '2d', ['Yesterday', 2, 'day'] ],
   [ '1d', ['Last 24 hours', 1, 'day'] ],
   [ 'today', ['Today'] ]
   ]);

   datesKeys.forEach((value) => {
     const currVal = dateKeyMap.get(value);
     const label = currVal[0];
     let start = moment().subtract(currVal[1], currVal[2]).utc();
     switch(label) {
      case 'Today':
        start = moment().startOf('day');
        break;
      case 'Yesterday':
        start = moment().subtract(1, 'day').startOf('day');
        break;
     }
     const end = (label === 'Yesterday') ? moment().subtract(1, 'days').endOf('day') : moment();
     const isActive = duration === value;
     newRangeArr.push({ name: label, value, start, end, isActive });
   });

  newRangeArr.push(defaultCustomRange);
  return newRangeArr;
}

/**
 * Returns a sample JSON anomaly eval object
 * @returns {Object}
 */
export function evalObj() {
  return {
    userReportAnomaly: 0,
    totalResponses: 0,
    trueAnomalies: 0,
    recall: NaN,
    totalAlerts: 6,
    responseRate: 0.0,
    precision: 0.0,
    falseAlarm: 0,
    newTrend: 0,
    weightedPrecision: 0.0
  };
}

/**
 * Builds the request parameters for the metric data API call.
 * TODO: Document this inline for clarity.
 * @method buildMetricDataUrl
 * @param {Object} graphConfig - the metric settings
 * @returns {String} metric data call params/url
 */
export function buildMetricDataUrl(graphConfig) {
  const { id, maxTime, filters, dimension, granularity } = graphConfig;
  // Chosen dimension
  const selectedDimension = dimension || 'All';
  // Do not send a filters param if value not present
  const filterQs = filters ? `&filters=${encodeURIComponent(filters)}` : '';
  // Load only a week of data if granularity is high
  const startTimeBucket = granularity && granularity.toLowerCase().includes('minute') ? 'week' : 'months';
  // For end date, choose either maxTime or end of yesterday
  const currentEnd = moment(maxTime).isValid() ? moment(maxTime).valueOf() : buildDateEod(1, 'day').valueOf();
  // For graph start date, take either 1 week or 1 month, depending on granularity
  const currentStart = moment(currentEnd).subtract(1, startTimeBucket).valueOf();
  // Baseline starts 1 week before our start date
  const baselineStart = moment(currentStart).subtract(1, 'week').valueOf();
  // Baseline ends 1 week before our end date
  const baselineEnd = moment(currentEnd).subtract(1, 'week');
  // Now build the metric data url
  return `/timeseries/compare/${id}/${currentStart}/${currentEnd}/${baselineStart}/${baselineEnd}?dimension=` +
         `${selectedDimension}&granularity=${granularity}${filterQs}`;
}

/**
 * If a dimension has been selected, the metric data object will contain subdimensions.
 * This method averages each subdimension's total change rate and returns a sorted list
 * of the top X graph-ready dimension objects
 * @method getTopDimensions
 * @param {Object} metricData - the graphable metric data returned from fetchAnomalyGraphData()
 * @param {Number} dimCount - number of dimensions to allow in response
 * @return {undefined}
 */
export function getTopDimensions(metricData, dimCount) {
  const colors = ['orange', 'teal', 'purple', 'red', 'green', 'pink'];
  const dimensionObj = metricData.subDimensionContributionMap || {};
  const dimensionKeys = Object.keys(dimensionObj);
  let processedDimensions = [];
  let dimensionList = [];
  let colorIndex = 0;

  // Build the array of subdimension objects for the selected dimension
  dimensionKeys.forEach((subDimension) => {
    let subdObj = dimensionObj[subDimension];
    let changeArr = subdObj.cumulativePercentageChange.map(item => Math.abs(item));
    let average = changeArr.reduce((previous, current) => current += previous) / changeArr.length;
    if (subDimension.toLowerCase() !== 'all') {
      dimensionList.push({
        average,
        name: subDimension,
        baselineValues: subdObj.baselineValues,
        currentValues: subdObj.currentValues,
        isSelected: true
      });
    }
  });
  processedDimensions = dimensionList.sortBy('average').reverse().slice(0, dimCount);
  processedDimensions.forEach((dimension) => {
    dimension.color = colors[colorIndex];
    colorIndex = colorIndex > 5 ? 0 : colorIndex + 1;
  });

  // Return the top X sorted by level of change contribution
  return processedDimensions;
}

/**
 * Data needed to render the stats 'cards' above the anomaly graph for a given alert
 * @param {Object} alertEvalMetrics - contains the alert's performance data
 * @param {Object} anomalyStats - collection of metric block definitions
 * example: {
 *    title: 'Recall',
      key: 'recall',
      units: '%',
      tooltip,
      text: 'Number of anomalies detected by the system.'
    }
 * @param {String} severity - the severity threshold entered
 * @param {Boolean} isPercent - suffix associated with the selected severity mode
 * @returns {Array}
 */
export function buildAnomalyStats(alertEvalMetrics, anomalyStats, showProjected = true) {
  anomalyStats.forEach((stat) => {
    let origData = alertEvalMetrics.current[stat.key];
    let newData = alertEvalMetrics.projected ? alertEvalMetrics.projected[stat.key] : null;
    let isPercentageMetric = stat.units === '%';
    let isTotal = stat.key === 'totalAlerts';
    stat.showProjected = showProjected;
    stat.value = isTotal ? origData : formatEvalMetric(origData, isPercentageMetric);
    stat.valueUnits = isFinite(origData) ? stat.units : null;
    if (isPresent(newData)) {
      stat.projected = isTotal ? newData : formatEvalMetric(newData, isPercentageMetric);
      stat.projectedUnits = isFinite(newData) ? stat.units : null;
      stat.showDirectionIcon = isFinite(origData) && isFinite(newData) && origData !== newData;
      stat.direction = stat.showDirectionIcon && origData > newData ? 'bottom' : 'top';
    }
  });

  return anomalyStats;
}

/**
 * Returns selected alert object properties for config group table display
 * @param {Object} alertData - a single alert record
 * @param {Number} alertIndex - record index
 * @returns {Array}
 */
export function formatConfigGroupProps(alertData, alertIndex) {
  return {
    number: alertIndex + 1,
    id: alertData.id,
    name: alertData.functionName,
    metric: alertData.metric + '::' + alertData.collection,
    owner: alertData.createdBy || 'N/A',
    status: alertData.isActive ? 'active' : 'inactive',
    isNewId: alertData.id === 0
  };
}

/**
 * When fetching current and projected MTTD (minimum time to detect) data, we need to supply the
 * endpoint with a severity threshold. This decides whether to use the default or not.
 * @method extractSeverity
 * @param {Number} defaultSeverity - number to fall back on if we have none defined in alert filter
 * @return {undefined}
 */
export function extractSeverity(alertData, defaultSeverity) {
  const alertFilterSeverity = getWithDefault(alertData, 'alertFilter.mttd', null);
  const parsedSeverity = alertFilterSeverity ? alertFilterSeverity.split(';')[1].split('=') : null;
  const isSeverityNumeric = parsedSeverity && !isNaN(parsedSeverity[1]);
  const finalSeverity = isSeverityNumeric ? parsedSeverity[1] : defaultSeverity;
  return finalSeverity;
}

export default {
  formatEvalMetric,
  toIdGroups,
  pluralizeTime,
  enhanceAnomalies,
  getTopDimensions,
  setUpTimeRangeOptions,
  formatConfigGroupProps,
  buildAnomalyStats,
  buildMetricDataUrl,
  extractSeverity,
  evalObj
};
<commit_msg>TE Graph dimension fix for Alert Page (#2848)<commit_after>import moment from 'moment';
import { isPresent } from "@ember/utils";
import { getWithDefault } from '@ember/object';
import { buildDateEod } from 'thirdeye-frontend/utils/utils';
import { getFormatedDuration } from 'thirdeye-frontend/utils/anomaly';
import floatToPercent from 'thirdeye-frontend/utils/float-to-percent';

/**
 * Handles types and defaults returned from eval/projected endpoints
 * @param {Number|String} metric - number or string like 'NaN', 'Infinity'
 * @param {Boolean} isPercentage - shall we treat this as a % or a whole number?
 * @returns {Object}
 */
export function formatEvalMetric(metric, isPercentage = false) {
  const isWhole = Number.isInteger(Number(metric));
  let shown = (metric === 'Infinity') ? metric : 'N/A';
  const multiplier = isPercentage ? 100 : 1;
  const convertedNum = metric * multiplier;
  const formattedNum = isWhole ? convertedNum : convertedNum.toFixed(1);
  let displayNum = isFinite(metric) ? formattedNum : shown;
  // Prevent meaninglessly large numbers
  return (Number(displayNum) > 10000) ? 'N/A' : displayNum;
}

/**
 * Pluralizes and formats the anomaly range duration string
 * @param {Number} time
 * @param {String} unit
 * @returns {String}
 */
export function pluralizeTime(time, unit) {
  const unitStr = time > 1 ? unit + 's' : unit;
  return time ? time + ' ' + unitStr : '';
}

/**
 * Split array of all anomalyIds into buckets of bucketSize
 * @param {Array} anomalyIds - array of anomaly Ids
 * @param {Number} bucketSize - number of anomalies per group
 * @returns {Array}
 */
export function toIdGroups(anomalyIds, bucketSize = 10) {
  const idGroups = anomalyIds.map((item, index) => {
    return (index % bucketSize === 0) ? anomalyIds.slice(index, index + bucketSize) : null;
  }).filter(item => item);
  return idGroups;
}

/**
 * Derives and formats extra anomaly properties such as duration
 * @param {Array} anomalies - array of raw anomalies
 * @returns {Array}
 */
export function enhanceAnomalies(rawAnomalies, severityScores) {
  const newAnomalies = [];
  const anomaliesPresent = rawAnomalies && rawAnomalies.length;
  // De-dupe raw anomalies, extract only the good stuff (anomalyDetailsList)
  const anomalies = anomaliesPresent ? [].concat(...rawAnomalies.map(data => data.anomalyDetailsList)) : [];
  // Extract all resolved scores from the RSVP promise response
  const resolvedScores = severityScores ? severityScores.map((score) => {
    return (score.state === 'fulfilled') ? score.value : '';
  }) : [];

  // Loop over all anomalies to configure display settings
  anomalies.forEach((anomaly) => {
    let dimensionList = [];
    let targetAnomaly = resolvedScores.find(score => Number(score.id) === Number(anomaly.anomalyId));
    // Extract current anomaly's score from array of all scores
    const score = resolvedScores.length && targetAnomaly ? targetAnomaly.score : null;
    // Set up anomaly change rate display
    const changeRate = (anomaly.current && anomaly.baseline) ? floatToPercent((anomaly.current - anomaly.baseline) / anomaly.baseline) : 0;
    const isNullChangeRate = Number.isNaN(Number(changeRate));
    // Set 'not reviewed' label
    if (!anomaly.anomalyFeedback) {
      anomaly.anomalyFeedback = 'Not reviewed yet';
    }
    // Add missing properties
    Object.assign(anomaly, {
      changeRate,
      isNullChangeRate,
      shownChangeRate: changeRate,
      isUserReported: anomaly.anomalyResultSource === 'USER_LABELED_ANOMALY',
      startDateStr: moment(anomaly.anomalyStart).format('MMM D, hh:mm A'),
      durationStr: getFormatedDuration(anomaly.anomalyStart, anomaly.anomalyEnd),
      severityScore: score && !isNaN(score) ? score.toFixed(2) : 'N/A',
      shownCurrent: Number(anomaly.current) > 0 ? anomaly.current : 'N/A',
      shownBaseline: Number(anomaly.baseline) > 0 ? anomaly.baseline : 'N/A',
      showResponseSaved: false,
      showResponseFailed: false
    });
    // Create a list of all available dimensions for toggling. Also massage dimension property.
    if (anomaly.anomalyFunctionDimension) {
      let dimensionObj = JSON.parse(anomaly.anomalyFunctionDimension);
      let dimensionStrArr = [];
      for (let dimension of Object.keys(dimensionObj)) {
        let dimensionKey = dimension.dasherize();
        let dimensionVal = dimensionObj[dimension].join(',');
        dimensionList.push({ dimensionKey, dimensionVal });
        dimensionStrArr.push(`${dimensionKey}:${dimensionVal}`);
      }
      let dimensionString = dimensionStrArr.join(' & ');
      Object.assign(anomaly, { dimensionList, dimensionString });
    }
    newAnomalies.push(anomaly);
  });
  // List most recent anomalies first
  return newAnomalies.sortBy('anomalyStart').reverse();
}

/**
 * Generates time range options for selection in the self-serve UI
 * @example output
 * [{ name: "3 Months", value: "3m", start: Moment, isActive: true },
 *  { name: "Custom", value: "custom", start: null, isActive: false }]
 * @method setUpTimeRangeOptions
 * @param {Array} datesKeys - array of keys used to generate time ranges
 * @param {String} duration - the selected time span that is default
 * @return {Array}
 */
export function setUpTimeRangeOptions(datesKeys, duration) {
  const newRangeArr = [];

  const defaultCustomRange = {
    name: 'Custom',
    value: 'custom',
    start: null,
    isActive: !datesKeys.includes(duration)
  };

  const dateKeyMap = new Map([
   [ '1m', ['Last 30 Days', 1, 'month'] ],
   [ '3m', ['3 Months', 3, 'month'] ],
   [ '2w', ['Last 2 Weeks', 2, 'week'] ],
   [ '1w', ['Last Week', 1, 'week'] ],
   [ '2d', ['Yesterday', 2, 'day'] ],
   [ '1d', ['Last 24 hours', 1, 'day'] ],
   [ 'today', ['Today'] ]
   ]);

   datesKeys.forEach((value) => {
     const currVal = dateKeyMap.get(value);
     const label = currVal[0];
     let start = moment().subtract(currVal[1], currVal[2]).utc();
     switch(label) {
      case 'Today':
        start = moment().startOf('day');
        break;
      case 'Yesterday':
        start = moment().subtract(1, 'day').startOf('day');
        break;
     }
     const end = (label === 'Yesterday') ? moment().subtract(1, 'days').endOf('day') : moment();
     const isActive = duration === value;
     newRangeArr.push({ name: label, value, start, end, isActive });
   });

  newRangeArr.push(defaultCustomRange);
  return newRangeArr;
}

/**
 * Returns a sample JSON anomaly eval object
 * @returns {Object}
 */
export function evalObj() {
  return {
    userReportAnomaly: 0,
    totalResponses: 0,
    trueAnomalies: 0,
    recall: NaN,
    totalAlerts: 6,
    responseRate: 0.0,
    precision: 0.0,
    falseAlarm: 0,
    newTrend: 0,
    weightedPrecision: 0.0
  };
}

/**
 * Builds the request parameters for the metric data API call.
 * TODO: Document this inline for clarity.
 * @method buildMetricDataUrl
 * @param {Object} graphConfig - the metric settings
 * @returns {String} metric data call params/url
 */
export function buildMetricDataUrl(graphConfig) {
  const { id, maxTime, filters, dimension, granularity } = graphConfig;
  // Chosen dimension
  const selectedDimension = dimension || 'All';
  // Do not send a filters param if value not present
  const filterQs = filters ? `&filters=${encodeURIComponent(filters)}` : '';
  // Load only a week of data if granularity is high
  const startTimeBucket = granularity && granularity.toLowerCase().includes('minute') ? 'week' : 'months';
  // For end date, choose either maxTime or end of yesterday
  const currentEnd = moment(maxTime).isValid() ? moment(maxTime).valueOf() : buildDateEod(1, 'day').valueOf();
  // For graph start date, take either 1 week or 1 month, depending on granularity
  const currentStart = moment(currentEnd).subtract(1, startTimeBucket).valueOf();
  // Baseline starts 1 week before our start date
  const baselineStart = moment(currentStart).subtract(1, 'week').valueOf();
  // Baseline ends 1 week before our end date
  const baselineEnd = moment(currentEnd).subtract(1, 'week');
  // Now build the metric data url
  return `/timeseries/compare/${id}/${currentStart}/${currentEnd}/${baselineStart}/${baselineEnd}?dimension=` +
         `${selectedDimension}&granularity=${granularity}${filterQs}`;
}

/**
 * If a dimension has been selected, the metric data object will contain subdimensions.
 * This method averages each subdimension's total change rate and returns a sorted list
 * of the top X graph-ready dimension objects
 * @method getTopDimensions
 * @param {Object} metricData - the graphable metric data returned from fetchAnomalyGraphData()
 * @param {Number} dimCount - number of dimensions to allow in response
 * @return {undefined}
 */
export function getTopDimensions(metricData, dimCount) {
  const colors = ['orange', 'teal', 'purple', 'red', 'green', 'pink'];
  const dimensionObj = metricData.subDimensionContributionMap || {};
  const dimensionKeys = Object.keys(dimensionObj);
  let processedDimensions = [];
  let dimensionList = [];
  let colorIndex = 0;

  // Build the array of subdimension objects for the selected dimension
  dimensionKeys.forEach((subDimension) => {
    let subdObj = dimensionObj[subDimension];
    let changeArr = subdObj.cumulativePercentageChange.map(item => Math.abs(item));
    let average = changeArr.length ? changeArr.reduce((previous, current) => current += previous) / changeArr.length : 0;
    if (subDimension.toLowerCase() !== 'all') {
      dimensionList.push({
        average,
        name: subDimension,
        baselineValues: subdObj.baselineValues,
        currentValues: subdObj.currentValues,
        isSelected: true
      });
    }
  });
  processedDimensions = dimensionList.sortBy('average').reverse().slice(0, dimCount);
  processedDimensions.forEach((dimension) => {
    dimension.color = colors[colorIndex];
    colorIndex = colorIndex > 5 ? 0 : colorIndex + 1;
  });

  // Return the top X sorted by level of change contribution
  return processedDimensions;
}

/**
 * Data needed to render the stats 'cards' above the anomaly graph for a given alert
 * @param {Object} alertEvalMetrics - contains the alert's performance data
 * @param {Object} anomalyStats - collection of metric block definitions
 * example: {
 *    title: 'Recall',
      key: 'recall',
      units: '%',
      tooltip,
      text: 'Number of anomalies detected by the system.'
    }
 * @param {String} severity - the severity threshold entered
 * @param {Boolean} isPercent - suffix associated with the selected severity mode
 * @returns {Array}
 */
export function buildAnomalyStats(alertEvalMetrics, anomalyStats, showProjected = true) {
  anomalyStats.forEach((stat) => {
    let origData = alertEvalMetrics.current[stat.key];
    let newData = alertEvalMetrics.projected ? alertEvalMetrics.projected[stat.key] : null;
    let isPercentageMetric = stat.units === '%';
    let isTotal = stat.key === 'totalAlerts';
    stat.showProjected = showProjected;
    stat.value = isTotal ? origData : formatEvalMetric(origData, isPercentageMetric);
    stat.valueUnits = isFinite(origData) ? stat.units : null;
    if (isPresent(newData)) {
      stat.projected = isTotal ? newData : formatEvalMetric(newData, isPercentageMetric);
      stat.projectedUnits = isFinite(newData) ? stat.units : null;
      stat.showDirectionIcon = isFinite(origData) && isFinite(newData) && origData !== newData;
      stat.direction = stat.showDirectionIcon && origData > newData ? 'bottom' : 'top';
    }
  });

  return anomalyStats;
}

/**
 * Returns selected alert object properties for config group table display
 * @param {Object} alertData - a single alert record
 * @param {Number} alertIndex - record index
 * @returns {Array}
 */
export function formatConfigGroupProps(alertData, alertIndex) {
  return {
    number: alertIndex + 1,
    id: alertData.id,
    name: alertData.functionName,
    metric: alertData.metric + '::' + alertData.collection,
    owner: alertData.createdBy || 'N/A',
    status: alertData.isActive ? 'active' : 'inactive',
    isNewId: alertData.id === 0
  };
}

/**
 * When fetching current and projected MTTD (minimum time to detect) data, we need to supply the
 * endpoint with a severity threshold. This decides whether to use the default or not.
 * @method extractSeverity
 * @param {Number} defaultSeverity - number to fall back on if we have none defined in alert filter
 * @return {undefined}
 */
export function extractSeverity(alertData, defaultSeverity) {
  const alertFilterSeverity = getWithDefault(alertData, 'alertFilter.mttd', null);
  const parsedSeverity = alertFilterSeverity ? alertFilterSeverity.split(';')[1].split('=') : null;
  const isSeverityNumeric = parsedSeverity && !isNaN(parsedSeverity[1]);
  const finalSeverity = isSeverityNumeric ? parsedSeverity[1] : defaultSeverity;
  return finalSeverity;
}

export default {
  formatEvalMetric,
  toIdGroups,
  pluralizeTime,
  enhanceAnomalies,
  getTopDimensions,
  setUpTimeRangeOptions,
  formatConfigGroupProps,
  buildAnomalyStats,
  buildMetricDataUrl,
  extractSeverity,
  evalObj
};
<|endoftext|>


<commit_before>import React from 'react'
import Event from './event'
import Details from './details'
import './index.scss'

class Events extends React.Component {
  static propTypes = {
    days: React.PropTypes.arrayOf(React.PropTypes.object),
    other: React.PropTypes.string,
  }

  constructor (props) {
    super(props)
    const now = new Date()
    console.log(props)
    this.state = {
      activeDetails: null,
      activeDate: new Date('08/25/2016').setHours(0, 0, 0, 0) === now.setHours(0, 0, 0, 0)
        ? 'Friday'
        : 'Thursday',
    }
  }

  onChangeDay = day => () => {
    this.setState({
      activeDetails: null,
      activeDate: day,
    })
  }

  onOpenTrackDetails = details => () => {
    this.setState({ activeDetails: details })
  }

  onPrevious = () => {
    this.navigate(-1)
  }

  onNext = () => {
    this.navigate(1)
  }

  calculateNextTrack (direction, indicies) {
    const { activeDetails } = this.state
    const current = indicies || { ...activeDetails }

    const { days } = this.props

    const slots = days[current.day].slots
    const tracks = slots[current.slot].tracks

    current.track += direction

    if (current.track >= tracks.length) {
      current.slot += direction
      current.track = -1

      if (!slots[current.slot]) {
        return activeDetails
      }

      return this.calculateNextTrack(direction, current)
    }

    if (current.track < 0) {
      current.slot += direction

      if (!slots[current.slot]) {
        return activeDetails
      }

      current.track = slots[current.slot].tracks.length
      return this.calculateNextTrack(direction, current)
    }

    if (tracks[current.track].grayed) {
      return this.calculateNextTrack(direction, current)
    }

    return current
  }

  navigate (direction) {
    if (this.state.activeDetails) {
      this.setState({
        activeDetails: this.calculateNextTrack(direction),
      })
    }
  }

  render () {
    const { days, other } = this.props
    const { activeDate, activeDetails } = this.state

    const hasDetails = !! activeDetails
    let detailsProps = {}

    if (hasDetails) {
      const { day, slot, track } = activeDetails
      detailsProps = days[day].slots[slot].tracks[track]
      detailsProps.time = days[day].slots[slot].time
    }

    return (

        <nav className={`Events-tabs${!hasDetails ? ' is-centered' : ''}`}>
          {days.map(({ date }) => {
            const weekday = date.split(/,?\s+/)[0]
            const classes = `Events-tab${activeDate === weekday ? ' is-active' : ''}`
            return (
              <button key={date} onClick={this.onChangeDay(weekday)} className={classes}>{weekday}</button>
            )
          })}
          <button
            onClick={this.onChangeDay('Other')}
            className={`Events-tab${activeDate === 'Other' ? ' is-active' : ''}`}
          >Community / SO</button>

        <div className={`Events-group${!hasDetails ? ' is-centered' : ''}`}>
          {days.map((day, index) =>
            <Event
              key={day.date}
              active={activeDetails}
              isActive={activeDate === day.date.split(/,?\s+/)[0]}
              date={day.date}
              slots={day.slots}
              index={index}
              onOpenTrackDetails={this.onOpenTrackDetails}
            />
          )}
          <div
            className={`Event${activeDate === 'Other' ? ' is-active' : ''}`}
            dangerouslySetInnerHTML={{ __html: other }}
          />

    )
  }
}

export default Events
<commit_msg>Add keyboard events<commit_after>import React from 'react'
import Event from './event'
import Details from './details'
import './index.scss'

class Events extends React.Component {
  static propTypes = {
    days: React.PropTypes.arrayOf(React.PropTypes.object),
    other: React.PropTypes.string,
  }

  constructor (props) {
    super(props)
    const now = new Date()
    this.state = {
      activeDetails: null,
      activeDate: new Date('08/25/2016').setHours(0, 0, 0, 0) === now.setHours(0, 0, 0, 0)
        ? 'Friday'
        : 'Thursday',
    }
  }

  componentDidMount () {
    window.addEventListener('keyup', this.onKeyUp)
  }

  componentWillUnmount () {
    window.removeEventListener('keyup', this.onKeyUp)
  }

  onKeyUp = ({ keyCode }) => {
    switch (keyCode) {
      case 37:
        this.onPrevious()
        break
      case 39:
        this.onNext()
        break
      default:
        break
    }
  }

  onChangeDay = day => () => {
    this.setState({
      activeDetails: null,
      activeDate: day,
    })
  }

  onOpenTrackDetails = details => () => {
    this.setState({ activeDetails: details })
  }

  onPrevious = () => {
    this.navigate(-1)
  }

  onNext = () => {
    this.navigate(1)
  }

  calculateNextTrack (direction, indicies) {
    const { activeDetails } = this.state
    const current = indicies || { ...activeDetails }

    const { days } = this.props

    const slots = days[current.day].slots
    const tracks = slots[current.slot].tracks

    current.track += direction

    if (current.track >= tracks.length) {
      current.slot += direction
      current.track = -1

      if (!slots[current.slot]) {
        return activeDetails
      }

      return this.calculateNextTrack(direction, current)
    }

    if (current.track < 0) {
      current.slot += direction

      if (!slots[current.slot]) {
        return activeDetails
      }

      current.track = slots[current.slot].tracks.length
      return this.calculateNextTrack(direction, current)
    }

    if (tracks[current.track].grayed) {
      return this.calculateNextTrack(direction, current)
    }

    return current
  }

  navigate (direction) {
    if (this.state.activeDetails) {
      this.setState({
        activeDetails: this.calculateNextTrack(direction),
      })
    }
  }

  render () {
    const { days, other } = this.props
    const { activeDate, activeDetails } = this.state

    const hasDetails = !! activeDetails
    let detailsProps = {}

    if (hasDetails) {
      const { day, slot, track } = activeDetails
      detailsProps = days[day].slots[slot].tracks[track]
      detailsProps.time = days[day].slots[slot].time
    }

    return (

        <nav className={`Events-tabs${!hasDetails ? ' is-centered' : ''}`}>
          {days.map(({ date }) => {
            const weekday = date.split(/,?\s+/)[0]
            const classes = `Events-tab${activeDate === weekday ? ' is-active' : ''}`
            return (
              <button key={date} onClick={this.onChangeDay(weekday)} className={classes}>{weekday}</button>
            )
          })}
          <button
            onClick={this.onChangeDay('Other')}
            className={`Events-tab${activeDate === 'Other' ? ' is-active' : ''}`}
          >Community / SO</button>

        <div className={`Events-group${!hasDetails ? ' is-centered' : ''}`}>
          {days.map((day, index) =>
            <Event
              key={day.date}
              active={activeDetails}
              isActive={activeDate === day.date.split(/,?\s+/)[0]}
              date={day.date}
              slots={day.slots}
              index={index}
              onOpenTrackDetails={this.onOpenTrackDetails}
            />
          )}
          <div
            className={`Event${activeDate === 'Other' ? ' is-active' : ''}`}
            dangerouslySetInnerHTML={{ __html: other }}
          />

    )
  }
}

export default Events
<|endoftext|>


<commit_before>// @flow

export type Resource = number;

export type ResourceFactory = {
  color: Resource,
  cost: Array<Resource>,
  points: number
};

export type PlayerHand = {
  factories: Array<ResourceFactory>,
  resources: Array<Resource>
};

export type Player = {
  name: string,
  hand: PlayerHand,
};

export type NoSelection = {|
  type: 'NO_SELECTION',
|}

export type FactorySelection = {|
  type: 'FACTORY_SELECTION',
  row: number,
  item: number,
|};

export type ResourceSelection = {|
  type: 'RESOURCE_SELECTION',
  selection: Array<Resource>
|}

export type Selection = NoSelection | FactorySelection | ResourceSelection;

export type Board = {
  factoriesByRow: Array<Array<ResourceFactory>>,
  resources: Array<Resource>,
  selection: Selection
};

export type PlayerState = {
  hand: PlayerHand,
};

export type GameState = {
  round: number,
  board: Board,
  playerState: Array<PlayerState>,
  currentPlayerIndex: ?number
};

export type UiMessage = {
  text: string,
  severity: 'INFO' | 'ERROR'
}

export type UiState = {
  asyncInProgress: boolean,
  message: ?UiMessage,
};

export type GameRef = {
  gameId: string,
  playerToken: string
}

export type State = {
  gameRef: ?GameRef,
  players: Array<Player>,
  gameState: ?GameState,
  ui: UiState
};
<commit_msg>Removing 'round' field from GameState type.<commit_after>// @flow

export type Resource = number;

export type ResourceFactory = {
  color: Resource,
  cost: Array<Resource>,
  points: number
};

export type PlayerHand = {
  factories: Array<ResourceFactory>,
  resources: Array<Resource>
};

export type Player = {
  name: string,
  hand: PlayerHand,
};

export type NoSelection = {|
  type: 'NO_SELECTION',
|}

export type FactorySelection = {|
  type: 'FACTORY_SELECTION',
  row: number,
  item: number,
|};

export type ResourceSelection = {|
  type: 'RESOURCE_SELECTION',
  selection: Array<Resource>
|}

export type Selection = NoSelection | FactorySelection | ResourceSelection;

export type Board = {
  factoriesByRow: Array<Array<ResourceFactory>>,
  resources: Array<Resource>,
  selection: Selection
};

export type PlayerState = {
  hand: PlayerHand,
};

export type GameState = {
  board: Board,
  playerState: Array<PlayerState>,
  currentPlayerIndex: ?number
};

export type UiMessage = {
  text: string,
  severity: 'INFO' | 'ERROR'
}

export type UiState = {
  asyncInProgress: boolean,
  message: ?UiMessage,
};

export type GameRef = {
  gameId: string,
  playerToken: string
}

export type State = {
  gameRef: ?GameRef,
  players: Array<Player>,
  gameState: ?GameState,
  ui: UiState
};
<|endoftext|>


<commit_before>import Victor from 'victor'
import { shapeOptions } from './Shape'
import Effect from './Effect'
import { rotate, offset } from '../common/geometry'
import PolarMachine from '../features/machine/PolarMachine'
import RectMachine from '../features/machine/RectMachine'

const options = {
  ...shapeOptions,
  ...{
    maskMachine: {
      title: 'Mask shape',
      type: 'dropdown',
      choices: ['rectangle', 'circle'],
      onChange: (changes, attrs) => {
        if (changes.maskMachine === 'circle') {
          changes.rotation = 0

          const size = Math.min(attrs.startingWidth, attrs.startingHeight)
          changes.startingHeight = size
          changes.startingWidth = size
          changes.canRotate = false
          changes.canChangeHeight = false
        } else {
          changes.canRotate = true
          changes.canChangeHeight = true
        }

        return changes
      }
    },
    maskMinimizeMoves: {
      title: 'Try to minimize perimeter moves',
      type: 'checkbox'
    },
    maskBorder: {
      title: 'Draw border',
      type: 'checkbox'
    }
  }
}

export default class Mask extends Effect {
  constructor() {
    super('Mask')
  }

  getInitialState() {
    return {
      ...super.getInitialState(),
      ...{
        type: 'mask',
        selectGroup: 'effects',
        startingWidth: 100,
        startingHeight: 100,
        maskMinimizeMoves: false,
        maskMachine: 'rectangle',
        maskBorder: false
      }
    }
  }

  getVertices(state) {
    const width = state.shape.startingWidth
    const height = state.shape.startingHeight

    return [
      new Victor(-width/2, height/2),
      new Victor(width/2, height/2),
      new Victor(width/2, -height/2),
      new Victor(-width/2, -height/2),
      new Victor(-width/2, height/2),
    ]
  }

  applyEffect(effect, layer, vertices) {
    vertices = vertices.map(vertex => {
      return rotate(offset(vertex, -effect.offsetX, -effect.offsetY), effect.rotation)
    })

    if (!layer.dragging && !effect.dragging) {
      const machineClass = effect.maskMachine === 'circle' ? PolarMachine : RectMachine

      const machine = new machineClass(
        vertices,
        { minX: 0, maxX: effect.startingWidth, minY: 0, maxY: effect.startingHeight, minimizeMoves: effect.maskMinimizeMoves, maxRadius: effect.startingWidth/2, perimeterConstant: effect.maskPerimeterConstant, mask: true },
        { border: effect.maskBorder })
      vertices = machine.polish().vertices
    }

    return vertices.map(vertex => {
      return offset(rotate(vertex, -effect.rotation), effect.offsetX, effect.offsetY)
    })
  }

  getOptions() {
    return options
  }
}
<commit_msg>Outline a circular mask when dragging<commit_after>import Victor from 'victor'
import { shapeOptions } from './Shape'
import Effect from './Effect'
import { rotate, offset, circle } from '../common/geometry'
import PolarMachine from '../features/machine/PolarMachine'
import RectMachine from '../features/machine/RectMachine'

const options = {
  ...shapeOptions,
  ...{
    maskMachine: {
      title: 'Mask shape',
      type: 'dropdown',
      choices: ['rectangle', 'circle'],
      onChange: (changes, attrs) => {
        if (changes.maskMachine === 'circle') {
          changes.rotation = 0

          const size = Math.min(attrs.startingWidth, attrs.startingHeight)
          changes.startingHeight = size
          changes.startingWidth = size
          changes.canRotate = false
          changes.canChangeHeight = false
        } else {
          changes.canRotate = true
          changes.canChangeHeight = true
        }

        return changes
      }
    },
    maskMinimizeMoves: {
      title: 'Try to minimize perimeter moves',
      type: 'checkbox'
    },
    maskBorder: {
      title: 'Draw border',
      type: 'checkbox'
    }
  }
}

export default class Mask extends Effect {
  constructor() {
    super('Mask')
  }

  getInitialState() {
    return {
      ...super.getInitialState(),
      ...{
        type: 'mask',
        selectGroup: 'effects',
        startingWidth: 100,
        startingHeight: 100,
        maskMinimizeMoves: false,
        maskMachine: 'rectangle',
        maskBorder: false
      }
    }
  }

  getVertices(state) {
    const width = state.shape.startingWidth
    const height = state.shape.startingHeight

    if (state.shape.dragging && state.shape.maskMachine === 'circle') {
      return circle(width/2, 0)
    } else {
      return [
        new Victor(-width/2, height/2),
        new Victor(width/2, height/2),
        new Victor(width/2, -height/2),
        new Victor(-width/2, -height/2),
        new Victor(-width/2, height/2),
      ]
    }
  }

  applyEffect(effect, layer, vertices) {
    vertices = vertices.map(vertex => {
      return rotate(offset(vertex, -effect.offsetX, -effect.offsetY), effect.rotation)
    })

    if (!layer.dragging && !effect.dragging) {
      const machineClass = effect.maskMachine === 'circle' ? PolarMachine : RectMachine

      const machine = new machineClass(
        vertices,
        { minX: 0, maxX: effect.startingWidth, minY: 0, maxY: effect.startingHeight, minimizeMoves: effect.maskMinimizeMoves, maxRadius: effect.startingWidth/2, perimeterConstant: effect.maskPerimeterConstant, mask: true },
        { border: effect.maskBorder })
      vertices = machine.polish().vertices
    }

    return vertices.map(vertex => {
      return offset(rotate(vertex, -effect.rotation), effect.offsetX, effect.offsetY)
    })
  }

  getOptions() {
    return options
  }
}
<|endoftext|>


<commit_before>import { format } from 'url'
import React from 'react'
import PropTypes from 'prop-types'
import { withRouter } from 'next/router'

import { translate } from 'react-i18next'

class SearchInput extends React.PureComponent {
  static propTypes = {
    router: PropTypes.shape({
      push: PropTypes.func.isRequired,
      query: PropTypes.object.isRequired
    }).isRequired,

    placeholder: PropTypes.string,

    hasButton: PropTypes.bool,
    defaultQuery: PropTypes.object,

    i18n: PropTypes.shape({
      language: PropTypes.string.isRequired
    }).isRequired,
    t: PropTypes.func.isRequired
  }

  static defaultProps = {
    defaultValue: '',

    hasButton: false
  }

  onSubmit = event => {
    event.preventDefault()

    const { router, i18n, defaultQuery } = this.props

    const query = {
      ...defaultQuery,
      ...router.query,
      q: event.target.query.value
    }

    // Reset search to first page when firing a new query.
    // We don’t need p=1 in the query string, so let’s drop it.
    delete query.p

    const url = format({
      pathname: '/search',
      query
    })

    router.push(url, `/${i18n.language}${url}`)
  }

  render() {
    const { router, placeholder, hasButton, t } = this.props

    return (
      <form onSubmit={this.onSubmit}>
        <input
          type='text'
          name='query'
          defaultValue={router.query.q}
          placeholder={placeholder || t('components.SearchInput.placeholder')}
        />

        {hasButton && (
          <button type='submit' />
        )}

        <style jsx>{`
          form {
            width: 100%;
            position: relative;
          }

          input {
            display: block;
            width: 100%;
            border: 1px solid rgba(34, 36, 38, 0.15);
            border-radius: 2px;
            margin: 0;
            outline: 0;
            line-height: 2em;
            font-size: 1.5rem;
            padding: 10px 60px 10px 20px;

            &:focus,
            &:active {
              border-color: #85b7d9;
            }

            @media (max-width: 767px) {
              font-size: 1.2rem;
            }
          }

          button {
            display: block;
            position: absolute;
            right: 0;
            top: 0;
            bottom: 0;
            background: #fff;
            border: 0 none;
            cursor: pointer;
            width: 50px;
            padding: 0 5px;
            outline: 0;
            background: url('/static/images/icons/search.svg') center left no-repeat;
          }
        `}</style>
      </form>
    )
  }
}

export default translate()(withRouter(SearchInput))
<commit_msg>Update search input border color<commit_after>import { format } from 'url'
import React from 'react'
import PropTypes from 'prop-types'
import { withRouter } from 'next/router'

import { translate } from 'react-i18next'

class SearchInput extends React.PureComponent {
  static propTypes = {
    router: PropTypes.shape({
      push: PropTypes.func.isRequired,
      query: PropTypes.object.isRequired
    }).isRequired,

    placeholder: PropTypes.string,

    hasButton: PropTypes.bool,
    defaultQuery: PropTypes.object,

    i18n: PropTypes.shape({
      language: PropTypes.string.isRequired
    }).isRequired,
    t: PropTypes.func.isRequired
  }

  static defaultProps = {
    defaultValue: '',

    hasButton: false
  }

  onSubmit = event => {
    event.preventDefault()

    const { router, i18n, defaultQuery } = this.props

    const query = {
      ...defaultQuery,
      ...router.query,
      q: event.target.query.value
    }

    // Reset search to first page when firing a new query.
    // We don’t need p=1 in the query string, so let’s drop it.
    delete query.p

    const url = format({
      pathname: '/search',
      query
    })

    router.push(url, `/${i18n.language}${url}`)
  }

  render() {
    const { router, placeholder, hasButton, t } = this.props

    return (
      <form onSubmit={this.onSubmit}>
        <input
          type='text'
          name='query'
          defaultValue={router.query.q}
          placeholder={placeholder || t('components.SearchInput.placeholder')}
        />

        {hasButton && (
          <button type='submit' />
        )}

        <style jsx>{`
          @import 'colors';

          form {
            width: 100%;
            position: relative;
          }

          input {
            display: block;
            width: 100%;
            border: 1px solid $lightgrey;
            border-radius: 2px;
            margin: 0;
            outline: 0;
            line-height: 2em;
            font-size: 1.5rem;
            padding: 10px 60px 10px 20px;

            &:focus,
            &:active {
              border-color: #85b7d9;
            }

            @media (max-width: 767px) {
              font-size: 1.2rem;
            }
          }

          button {
            display: block;
            position: absolute;
            right: 0;
            top: 0;
            bottom: 0;
            background: #fff;
            border: 0 none;
            cursor: pointer;
            width: 50px;
            padding: 0 5px;
            outline: 0;
            background: url('/static/images/icons/search.svg') center left no-repeat;
          }
        `}</style>
      </form>
    )
  }
}

export default translate()(withRouter(SearchInput))
<|endoftext|>


<commit_before>'use strict';

const Knex = require('knex');
const { knexSnakeCaseMappers, Model } = require('objection');
const uuid = require('uuid');

const knex = Knex({
  client: 'pg',
  connection: {
    database : 'blog'
  },

  // Merge `postProcessResponse` and `wrapIdentifier` mappers.
  ...knexSnakeCaseMappers()
});

Model.knex(knex);

class Base extends Model {
  /**
   * Called before a model is inserted into the database.
   *
   * You can return a promise from this function if you need to do asynchronous stuff. You can also throw an exception
   * to abort the insert and reject the query. This can be useful if you need to do insert specific validation.
   *
   * @param {object} queryContext - Context object of the insert query. http://vincit.github.io/objection.js/#context
   */
  $beforeInsert() {
    // Strip values for immutable properties.
    const immutableProperties = this.constructor.immutableProperties;
    if (Array.isArray(immutableProperties)) {
      immutableProperties.forEach((key) => {
        delete this[key];
      });
    }

    // Auto-generate UUID if applicable.
    const uuidColumn = this.constructor.uuidColumn;
    if (uuidColumn && this.hasProperty(uuidColumn)) {
      this[uuidColumn] = uuid.v4();
    }

    // Set createdAt timestamp if applicable.
    if (this.hasProperty('createdAt')) {
      this.createdAt = new Date().toISOString();
    }
  }

  /**
   * Called before a model is updated.
   *
   * You can return a promise from this function if you need to do asynchronous stuff. You can also throw an exception
   * to abort the update and reject the query. This can be useful if you need to do update specific validation.
   *
   * This method is also called before a model is patched. Therefore all the model’s properties may not exist. You can
   * check if the update operation is a patch by checking the opt.patch boolean.
   *
   * The opt.old object contains the old values while this contains the updated values. The old values are never
   * fetched from the database implicitly. For non-instance queries the opt.old object is undefined.
   *
   * @param {ModelOptions} opt - Update options.
   * @param {object} queryContext - Context object of the update query. http://vincit.github.io/objection.js/#context
   */
  $beforeUpdate(opt) {
    // Strip values for immutable properties.
    const immutableProperties = this.constructor.immutableProperties;
    if (Array.isArray(immutableProperties)) {
      immutableProperties.forEach((key) => {
        delete this[key];
      });
    }

    // Set updatedAt timestamp if applicable.
    if (this.hasProperty('updatedAt')) {
      this.updatedAt = new Date().toISOString();
    }
  }

  /**
   * Check if model jsonSchema contains a given property.
   *
   * @param {string} property - Name of property to check for.
   * @return {boolean}
   */
  hasProperty(property) {
    const properties = this.constructor.jsonSchema && this.constructor.jsonSchema.properties;

    if (!properties) {
      return false;
    }

    return properties.hasOwnProperty(property);
  }

  /**
   * Default sort order used by getPaginated method.
   *
   * @return {string}
   */
  static get defaultOrder() {
    return 'ASC';
  }

  /**
   * Default sort property used by getPaginated method.
   *
   * @return {string}
   */
  static get defaultOrderBy() {
    return this.idColumn;
  }

  /**
   * Default page size used by getPaginated method.
   *
   * @return {number}
   */
  static get defaultPageSize() {
    return 100;
  }

  /**
   * Model properties that should be excluded from inserts and updates.
   *
   * These properties are deleted from the model $beforeInsert and $beforeUpdate. If the model has a createdAt
   * property, it will be set $beforeInsert. If the model has an updatedAt property, it will be set $beforeUpdate.
   *
   * @return {string[]}
   */
  static get immutableProperties() {
    return [
      this.idColumn,
      'createdAt',
      'updatedAt'
    ];
  }

  /**
   * Default page size used by getPaginated method.
   *
   * @return {number}
   */
  static get maxPageSize() {
    return 500;
  }

  /**
   * Create a new record.
   *
   * @param {object} attrs - Model properties based on this.jsonSchema.
   * @return {Promise<T>}
   */
  static create(attrs) {
    return this.query()
      .insert(attrs)
      .returning('*')
      .execute();
  }

  /**
   * Delete a record by ID.
   *
   * @param {*} id - ID of the record to delete based on this.idColumn.
   * @return {Promise<T>}
   */
  static delete(id) {
    return this
      .query()
      .delete()
      .first()
      .where(this.idColumn, id)
      .returning('*')
      .throwIfNotFound()
      .execute();
  }

  /**
   * Fetch a record by ID.
   *
   * @param {*} id - ID of the record to fetch based on this.idColumn.
   * @return {Promise<T>}
   */
  static getById(id) {
    return this.query()
      .findOne('id', id)
      .throwIfNotFound()
      .execute();
  }

  /**
   * Fetch a paginated set of records.
   *
   * @param {object} opts - Order and pagination options.
   * @return {Promise<T[]>}
   */
  static getPaginated(opts = {}) {
    const { order, orderBy, page, pageSize } = Object.assign({
      order: this.defaultOrder,
      orderBy: this.defaultOrderBy,
      page: 0,
      pageSize: this.defaultPageSize
    }, opts);

    return this.query()
      .page(page, Math.min(pageSize, this.maxPageSize))
      .orderBy(orderBy, order)
      .execute();
  }

  /**
   * Update a record by ID.
   *
   * @param {*} id - ID of the record to update based on this.idColumn.
   * @param {object} attrs - Model properties based on this.jsonSchema.
   * @return {Promise<T>}
   */
  static patch(id, attrs) {
    return this.query()
      .patch(attrs)
      .first()
      .where(this.idColumn, id)
      .returning('*')
      .throwIfNotFound()
      .execute();
  }
}

module.exports = Base;
<commit_msg>Refactor base model to include methods that just return QueryBuilder<commit_after>'use strict';

const Knex = require('knex');
const { knexSnakeCaseMappers, Model } = require('objection');
const uuid = require('uuid');

const knex = Knex({
  client: 'pg',
  connection: {
    database : 'blog'
  },

  // Merge `postProcessResponse` and `wrapIdentifier` mappers.
  ...knexSnakeCaseMappers()
});

Model.knex(knex);

class Base extends Model {
  /**
   * Called before a model is inserted into the database.
   *
   * You can return a promise from this function if you need to do asynchronous stuff. You can also throw an exception
   * to abort the insert and reject the query. This can be useful if you need to do insert specific validation.
   *
   * @param {object} queryContext - Context object of the insert query. http://vincit.github.io/objection.js/#context
   */
  $beforeInsert() {
    // Strip values for immutable properties.
    const immutableProperties = this.constructor.immutableProperties;
    if (Array.isArray(immutableProperties)) {
      immutableProperties.forEach((key) => {
        delete this[key];
      });
    }

    // Auto-generate UUID if applicable.
    const uuidColumn = this.constructor.uuidColumn;
    if (uuidColumn && this.hasProperty(uuidColumn)) {
      this[uuidColumn] = uuid.v4();
    }

    // Set createdAt timestamp if applicable.
    if (this.hasProperty('createdAt')) {
      this.createdAt = new Date().toISOString();
    }
  }

  /**
   * Called before a model is updated.
   *
   * You can return a promise from this function if you need to do asynchronous stuff. You can also throw an exception
   * to abort the update and reject the query. This can be useful if you need to do update specific validation.
   *
   * This method is also called before a model is patched. Therefore all the model’s properties may not exist. You can
   * check if the update operation is a patch by checking the opt.patch boolean.
   *
   * The opt.old object contains the old values while this contains the updated values. The old values are never
   * fetched from the database implicitly. For non-instance queries the opt.old object is undefined.
   *
   * @param {ModelOptions} opt - Update options.
   * @param {object} queryContext - Context object of the update query. http://vincit.github.io/objection.js/#context
   */
  $beforeUpdate(opt) {
    // Strip values for immutable properties.
    const immutableProperties = this.constructor.immutableProperties;
    if (Array.isArray(immutableProperties)) {
      immutableProperties.forEach((key) => {
        delete this[key];
      });
    }

    // Set updatedAt timestamp if applicable.
    if (this.hasProperty('updatedAt')) {
      this.updatedAt = new Date().toISOString();
    }
  }

  /**
   * Check if model jsonSchema contains a given property.
   *
   * @param {string} property - Name of property to check for.
   * @return {boolean}
   */
  hasProperty(property) {
    const properties = this.constructor.jsonSchema && this.constructor.jsonSchema.properties;

    if (!properties) {
      return false;
    }

    return properties.hasOwnProperty(property);
  }

  /**
   * Default sort order used by getPaginated method.
   *
   * @return {string}
   */
  static get defaultOrder() {
    return 'ASC';
  }

  /**
   * Default sort property used by getPaginated method.
   *
   * @return {string}
   */
  static get defaultOrderBy() {
    return this.idColumn;
  }

  /**
   * Default page size used by getPaginated method.
   *
   * @return {number}
   */
  static get defaultPageSize() {
    return 100;
  }

  /**
   * Model properties that should be excluded from inserts and updates.
   *
   * These properties are deleted from the model $beforeInsert and $beforeUpdate. If the model has a createdAt
   * property, it will be set $beforeInsert. If the model has an updatedAt property, it will be set $beforeUpdate.
   *
   * @return {string[]}
   */
  static get immutableProperties() {
    return [
      this.idColumn,
      'createdAt',
      'updatedAt'
    ];
  }

  /**
   * Default page size used by getPaginated method.
   *
   * @return {number}
   */
  static get maxPageSize() {
    return 500;
  }

  /**
   * Create a new record.
   *
   * @param {object} attrs - Model properties based on this.jsonSchema.
   * @return {Promise<T>}
   */
  static create(attrs) {
    return this.createQuery(attrs)
      .execute();
  }

  /**
   * Generate SQL query to create a new record.
   *
   * @param {object} attrs - Model properties based on this.jsonSchema.
   * @return {QueryBuilder} - Objection.js QueryBuilder object. http://vincit.github.io/objection.js/#querybuilder
   */
  static createQuery(attrs) {
    return this.query()
      .insert(attrs)
      .returning('*');
  }

  /**
   * Delete a record by ID.
   *
   * @param {*} id - ID of the record to delete based on this.idColumn.
   * @return {Promise<T>}
   */
  static delete(id) {
    return this.deleteQuery(id)
      .throwIfNotFound()
      .execute();
  }

  /**
   * Generate SQL query to delete a record by ID.
   *
   * @param {*} id - ID of the record to delete based on this.idColumn.
   * @return {QueryBuilder} - Objection.js QueryBuilder object. http://vincit.github.io/objection.js/#querybuilder
   */
  static deleteQuery(id) {
    return this
      .query()
      .delete()
      .first()
      .where(this.idColumn, id)
      .returning('*');
  }

  /**
   * Fetch a record by ID.
   *
   * @param {*} id - ID of the record to fetch based on this.idColumn.
   * @return {Promise<T>}
   */
  static getById(id) {
    return this.getByIdQuery(id)
      .throwIfNotFound()
      .execute();
  }

  /**
   * Generate SQL query to fetch a record by ID.
   *
   * @param {*} id - ID of the record to fetch based on this.idColumn.
   * @return {QueryBuilder} - Objection.js QueryBuilder object. http://vincit.github.io/objection.js/#querybuilder
   */
  static getByIdQuery(id) {
    return this.query()
      .findOne('id', id);
  }

  /**
   * Fetch a paginated set of records.
   *
   * @param {object} opts - Order and pagination options.
   * @return {Promise<T[]>}
   */
  static getPaginated(opts = {}) {
    return this.getPaginatedQuery(opts)
      .execute();
  }

  /**
   * Generate SQL query to fetch a paginated set of records.
   *
   * @param {object} opts - Order and pagination options.
   * @return {QueryBuilder} - Objection.js QueryBuilder object. http://vincit.github.io/objection.js/#querybuilder
   */
  static getPaginatedQuery(opts = {}) {
    const { order, orderBy, page, pageSize } = Object.assign({
      order: this.defaultOrder,
      orderBy: this.defaultOrderBy,
      page: 0,
      pageSize: this.defaultPageSize
    }, opts);

    return this.query()
      .page(page, Math.min(pageSize, this.maxPageSize))
      .orderBy(orderBy, order);
  }

  /**
   * Update a record by ID.
   *
   * @param {*} id - ID of the record to update based on this.idColumn.
   * @param {object} attrs - Model properties based on this.jsonSchema.
   * @return {Promise<T>}
   */
  static patch(id, attrs) {
    return this.patchQuery(id, attrs)
      .throwIfNotFound()
      .execute();
  }

  /**
   * Generate SQL query to update a record by ID.
   *
   * @param {*} id - ID of the record to update based on this.idColumn.
   * @param {object} attrs - Model properties based on this.jsonSchema.
   * @return {QueryBuilder} - Objection.js QueryBuilder object. http://vincit.github.io/objection.js/#querybuilder
   */
  static patchQuery(id, attrs) {
    return this.query()
      .patch(attrs)
      .first()
      .where(this.idColumn, id)
      .returning('*');
  }
}

module.exports = Base;
<|endoftext|>


<commit_before>'use strict';

/*
* When adding modules to the namespace, ensure that they are added in alphabetical order according to module name.
*/

// MODULES //

var setReadOnly = require( '@stdlib/utils/define-read-only-property' );

// MAIN //

/**
* Top-level namespace.
*
* @namespace ns
*/
var ns = {};

/**
* @name Float32Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/float32}
*/
setReadOnly( ns, 'Float32Array', require( '@stdlib/types/array/float32' ) );

/**
* @name Float64Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/float64}
*/
setReadOnly( ns, 'Float64Array', require( '@stdlib/types/array/float64' ) );

/**
* @name Int16Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/int16}
*/
setReadOnly( ns, 'Int16Array', require( '@stdlib/types/array/int16' ) );

/**
* @name Int32Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/int32}
*/
setReadOnly( ns, 'Int32Array', require( '@stdlib/types/array/int32' ) );

/**
* @name Int8Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/int8}
*/
setReadOnly( ns, 'Int8Array', require( '@stdlib/types/array/int8' ) );

/**
* @name reviveTypedArray
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/types/array/reviver}
*/
setReadOnly( ns, 'reviveTypedArray', require( '@stdlib/types/array/reviver' ) );

/**
* @name typedarray2json
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/types/array/to-json}
*/
setReadOnly( ns, 'typedarray2json', require( '@stdlib/types/array/to-json' ) );

/**
* @name typedarray
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/typed}
*/
setReadOnly( ns, 'typedarray', require( '@stdlib/types/array/typed' ) );

/**
* @name Uint16Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/uint16}
*/
setReadOnly( ns, 'Uint16Array', require( '@stdlib/types/array/uint16' ) );

/**
* @name Uint32Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/uint32}
*/
setReadOnly( ns, 'Uint32Array', require( '@stdlib/types/array/uint32' ) );

/**
* @name Uint8Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/uint8}
*/
setReadOnly( ns, 'Uint8Array', require( '@stdlib/types/array/uint8' ) );

/**
* @name Uint8ClampedArray
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/uint8c}
*/
setReadOnly( ns, 'Uint8ClampedArray', require( '@stdlib/types/array/uint8c' ) );

// EXPORTS //

module.exports = ns;
<commit_msg>Update namespace<commit_after>'use strict';

/*
* When adding modules to the namespace, ensure that they are added in alphabetical order according to module name.
*/

// MODULES //

var setReadOnly = require( '@stdlib/utils/define-read-only-property' );

// MAIN //

/**
* Top-level namespace.
*
* @namespace ns
*/
var ns = {};

/**
* @name ArrayBuffer
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/buffer}
*/
setReadOnly( ns, 'ArrayBuffer', require( '@stdlib/types/array/buffer' ) );

/**
* @name Float32Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/float32}
*/
setReadOnly( ns, 'Float32Array', require( '@stdlib/types/array/float32' ) );

/**
* @name Float64Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/float64}
*/
setReadOnly( ns, 'Float64Array', require( '@stdlib/types/array/float64' ) );

/**
* @name Int16Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/int16}
*/
setReadOnly( ns, 'Int16Array', require( '@stdlib/types/array/int16' ) );

/**
* @name Int32Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/int32}
*/
setReadOnly( ns, 'Int32Array', require( '@stdlib/types/array/int32' ) );

/**
* @name Int8Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/int8}
*/
setReadOnly( ns, 'Int8Array', require( '@stdlib/types/array/int8' ) );

/**
* @name reviveTypedArray
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/types/array/reviver}
*/
setReadOnly( ns, 'reviveTypedArray', require( '@stdlib/types/array/reviver' ) );

/**
* @name typedarray2json
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/types/array/to-json}
*/
setReadOnly( ns, 'typedarray2json', require( '@stdlib/types/array/to-json' ) );

/**
* @name typedarray
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/typed}
*/
setReadOnly( ns, 'typedarray', require( '@stdlib/types/array/typed' ) );

/**
* @name Uint16Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/uint16}
*/
setReadOnly( ns, 'Uint16Array', require( '@stdlib/types/array/uint16' ) );

/**
* @name Uint32Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/uint32}
*/
setReadOnly( ns, 'Uint32Array', require( '@stdlib/types/array/uint32' ) );

/**
* @name Uint8Array
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/uint8}
*/
setReadOnly( ns, 'Uint8Array', require( '@stdlib/types/array/uint8' ) );

/**
* @name Uint8ClampedArray
* @memberof ns
* @readonly
* @constructor
* @see {@link module:@stdlib/types/array/uint8c}
*/
setReadOnly( ns, 'Uint8ClampedArray', require( '@stdlib/types/array/uint8c' ) );

// EXPORTS //

module.exports = ns;
<|endoftext|>


<commit_before>export default {
  getMousePosition (event) {
    return {
      x: event.pageX - (window.scrollX || window.pageXOffset),
      y: event.pageY - (window.scrollY || window.pageYOffset),
    };
  },

  getTouchPosition (event) {
    return {
      x: event.touches[0].pageX - (window.scrollX || window.pageXOffset),
      y: event.touches[0].pageY - (window.scrollY || window.pageYOffset),
    };
  },

  pauseEvent (event) {
    event.stopPropagation();
    event.preventDefault();
  },

  addEventsToDocument (eventMap) {
    for (const key in eventMap) {
      document.addEventListener(key, eventMap[key], false);
    }
  },

  removeEventsFromDocument (eventMap) {
    for (const key in eventMap) {
      document.removeEventListener(key, eventMap[key], false);
    }
  },

  targetIsDescendant (event, parent) {
    let node = event.target;
    while (node !== null) {
      if (node === parent) {
        return true;
      }
      node = node.parentNode;
    }
    return false;
  },

  addEventListenerOnTransitionEnded (element, fn) {
    const eventName = transitionEventNamesFor(element);
    if (!eventName) {
      return false;
    }
    element.addEventListener(eventName, fn);
    return true;
  },

  removeEventListenerOnTransitionEnded (element, fn) {
    const eventName = transitionEventNamesFor(element);
    if (!eventName) {
      return false;
    }
    element.removeEventListener(eventName, fn);
    return true;
  },
};

const TRANSITIONS = {
  'transition': 'transitionend',
  'OTransition': 'oTransitionEnd',
  'MozTransition': 'transitionend',
  'WebkitTransition': 'webkitTransitionEnd',
};

function transitionEventNamesFor (element) {
  for (const transition in TRANSITIONS) {
    if (element && element.style[transition] !== undefined) {
      return TRANSITIONS[transition];
    }
  }
}
<commit_msg>Added extra functions to bind events to the window object<commit_after>export default {
  getMousePosition (event) {
    return {
      x: event.pageX - (window.scrollX || window.pageXOffset),
      y: event.pageY - (window.scrollY || window.pageYOffset),
    };
  },

  getTouchPosition (event) {
    return {
      x: event.touches[0].pageX - (window.scrollX || window.pageXOffset),
      y: event.touches[0].pageY - (window.scrollY || window.pageYOffset),
    };
  },

  pauseEvent (event) {
    event.stopPropagation();
    event.preventDefault();
  },

  addEventsToDocument (eventMap) {
    for (const key in eventMap) {
      document.addEventListener(key, eventMap[key], false);
    }
  },

  removeEventsFromDocument (eventMap) {
    for (const key in eventMap) {
      document.removeEventListener(key, eventMap[key], false);
    }
  },

  addEventsToWindow (eventMap) {
    for (const key in eventMap) {
      window.addEventListener(key, eventMap[key], false);
    }
  },

  removeEventsFromWindow (eventMap) {
    for (const key in eventMap) {
      window.removeEventListener(key, eventMap[key], false);
    }
  },

  targetIsDescendant (event, parent) {
    let node = event.target;
    while (node !== null) {
      if (node === parent) {
        return true;
      }
      node = node.parentNode;
    }
    return false;
  },

  addEventListenerOnTransitionEnded (element, fn) {
    const eventName = transitionEventNamesFor(element);
    if (!eventName) {
      return false;
    }
    element.addEventListener(eventName, fn);
    return true;
  },

  removeEventListenerOnTransitionEnded (element, fn) {
    const eventName = transitionEventNamesFor(element);
    if (!eventName) {
      return false;
    }
    element.removeEventListener(eventName, fn);
    return true;
  },
};

const TRANSITIONS = {
  'transition': 'transitionend',
  'OTransition': 'oTransitionEnd',
  'MozTransition': 'transitionend',
  'WebkitTransition': 'webkitTransitionEnd',
};

function transitionEventNamesFor (element) {
  for (const transition in TRANSITIONS) {
    if (element && element.style[transition] !== undefined) {
      return TRANSITIONS[transition];
    }
  }
}
<|endoftext|>


<commit_before><commit_msg>Removes used script<commit_after><|endoftext|>


<commit_before>'use strict';

var expect = chai.expect;
var event, el;
mocha.setup('bdd');

// Override window.confirm for the test runner.
window.confirm = function(message) { return true; };

describe('Code editor', function() {
    beforeEach(function() {
        event = document.createEvent('HTMLEvents');
    });

    it('should set a tab as active', function() {
        var el = document.getElementById('tabs').getElementsByClassName('js-tab');
        event.initEvent('click', true, false);
        el[0].dispatchEvent(event);
        var isActive = el[0].getAttribute('class').match(/active/);
        expect(isActive).to.not.be.null;
    });

    it('should delete a tab', function() {
        el = document.getElementsByClassName('js-deltab');
        event.initEvent('click', true, false);
        el[0].dispatchEvent(event);
        expect(el[0]).to.be.undefined;
    });

    describe('Tab creation', function() {
        beforeEach(function() {
            event = document.createEvent('HTMLEvents');
            el = document.getElementById('addtab');
        });

        it('should create a new tab', function() {
            var value = document.getElementById('addtab-filename').value = 'foo';
            event.initEvent('submit', true, false);
            el.dispatchEvent(event);
            var tab = document.getElementById('tabs').getElementsByClassName('js-tab');
            expect(tab[tab.length - 1].rel).to.equal('foo.mss');
        });

        it('should reject file formats in the filename', function() {
            var value = document.getElementById('addtab-filename').value = 'foo.mss';
            event.initEvent('submit', true, false);
            el.dispatchEvent(event);
            var tab = document.getElementById('tabs').getElementsByClassName('js-tab');
            expect(tab[tab.length - 1].rel).to.equal('foo.mss');
        });
    });
});

describe('Layers', function() {
    beforeEach(function() {
        event = document.createEvent('HTMLEvents');
    });

    it('should open layers description', function() {
        var el = document.getElementById('layers').getElementsByClassName('js-tab');
        event.initEvent('click', true, false);
        el[0].dispatchEvent(event);
        var isActive = el[0].getAttribute('class').match(/active/);
        expect(isActive).to.not.be.null;
    });
});

describe('Documentation', function() {
    beforeEach(function() {
        event = document.createEvent('HTMLEvents');
    });

    it('should tab through help topics', function() {
        var el = document.getElementById('docs').getElementsByClassName('js-tab');
        event.initEvent('click', true, false);
        var last = el[el.length - 1];
        last.dispatchEvent(event);
        var isActive = last.getAttribute('class').match(/active/);
        var docIsActive = document.getElementById(last.href.split('#')[1]).getAttribute('class').match(/active/);
        expect(isActive).to.not.be.null;
        expect(docIsActive).to.not.be.null;
    });
});

mocha.ignoreLeaks();

if (window.mochaPhantomJS) {
    mochaPhantomJS.run();
} else {
    mocha.run();
}
<commit_msg>Wrap everything in an Editor test, stub some stuff and save a project<commit_after>'use strict';

var expect = chai.expect;
var event, el;
mocha.setup('bdd');

// Override window.confirm for the test runner.
window.confirm = function(message) { return true; };

describe('Editor', function() {
    beforeEach(function() {
        event = document.createEvent('HTMLEvents');
    });

    it('saves a project', function() {
        el = document.getElementById('title').getElementsByClassName('js-save')[0];
        event.initEvent('click', true, false);
        el.dispatchEvent(event);
        var form = document.getElementById('saveas')
        form.getElementsByTagName('input')[0].value = 'foo.tm2';
        form.dispatchEvent(event);
    });

    it('loads a project');

    describe('Code editor', function() {
        beforeEach(function() {
            event = document.createEvent('HTMLEvents');
        });

        it('should set a tab as active', function() {
            el = document.getElementById('tabs').getElementsByClassName('js-tab');
            event.initEvent('click', true, false);
            el[0].dispatchEvent(event);
            var isActive = el[0].getAttribute('class').match(/active/);
            expect(isActive).to.not.be.null;
        });

        it('should delete a tab', function() {
            el = document.getElementsByClassName('js-deltab');
            event.initEvent('click', true, false);
            el[0].dispatchEvent(event);
            expect(el[0]).to.be.undefined;
        });

        describe('Tab creation', function() {
            beforeEach(function() {
                event = document.createEvent('HTMLEvents');
                el = document.getElementById('addtab');
            });

            it('should create a new tab', function() {
                var value = document.getElementById('addtab-filename').value = 'foo';
                event.initEvent('submit', true, false);
                el.dispatchEvent(event);
                var tab = document.getElementById('tabs').getElementsByClassName('js-tab');
                expect(tab[tab.length - 1].rel).to.equal('foo.mss');
            });

            it('should reject file formats in the filename', function() {
                var value = document.getElementById('addtab-filename').value = 'foo.mss';
                event.initEvent('submit', true, false);
                el.dispatchEvent(event);
                var tab = document.getElementById('tabs').getElementsByClassName('js-tab');
                expect(tab[tab.length - 1].rel).to.equal('foo.mss');
            });
        });
    });

    describe('Settings', function() {
        beforeEach(function() {
            event = document.createEvent('HTMLEvents');
        });

        it('should delete a project', function() {
            el = document.getElementById('settings-style').getElementsByClassName('js-ref-delete')[0];
            // event.initEvent('click', true, false);
            // el.dispatchEvent(event);

            console.log(el);
            // expect();
        });
    });

    describe('Layers', function() {
        beforeEach(function() {
            event = document.createEvent('HTMLEvents');
        });

        it('should open layers description', function() {
            var el = document.getElementById('layers').getElementsByClassName('js-tab');
            event.initEvent('click', true, false);
            el[0].dispatchEvent(event);
            var isActive = el[0].getAttribute('class').match(/active/);
            expect(isActive).to.not.be.null;
        });
    });

    describe('Documentation', function() {
        beforeEach(function() {
            event = document.createEvent('HTMLEvents');
        });

        it('should tab through help topics', function() {
            var el = document.getElementById('docs').getElementsByClassName('js-tab');
            event.initEvent('click', true, false);
            var last = el[el.length - 1];
            last.dispatchEvent(event);
            var isActive = last.getAttribute('class').match(/active/);
            var docIsActive = document.getElementById(last.href.split('#')[1]).getAttribute('class').match(/active/);
            expect(isActive).to.not.be.null;
            expect(docIsActive).to.not.be.null;
        });
    });
});

mocha.ignoreLeaks();

if (window.mochaPhantomJS) {
    mochaPhantomJS.run();
} else {
    mocha.run();
}
<|endoftext|>


<commit_before>'use strict';

// this Babel plugin removes configuration comments such as:
//   <!-- template-lint triple-curlies=false -->
//   <!-- template-lint enabled=false -->

module.exports = function() {
  function RemoveConfigurationHtmlCommentsPlugin() {}

  RemoveConfigurationHtmlCommentsPlugin.prototype.transform = function(ast) {
    this.syntax.traverse(ast, {
      CommentStatement: function(node) {
        if (node.value.trim().indexOf('template-lint ') === 0) {
          // remove the entry
          return null;
        }
      }
    });

    return ast;
  };

  return RemoveConfigurationHtmlCommentsPlugin;
};
<commit_msg>Revert "Use simpler traverse API for removing config comments."<commit_after>'use strict';

// this Babel plugin removes configuration comments such as:
//   <!-- template-lint triple-curlies=false -->
//   <!-- template-lint enabled=false -->

var AstNodeInfo = require('ember-template-lint/lib/helpers/ast-node-info');

module.exports = function() {
  function RemoveConfigurationHtmlCommentsPlugin() {}

  RemoveConfigurationHtmlCommentsPlugin.prototype.transform = function(ast) {
    var walker = new this.syntax.Walker();
    var bodyEntry;

    walker.visit(ast, function(node) {
      if (node.type === 'Program') {
        for (var i = 0; i < node.body.length; i++) {
          bodyEntry = node.body[i];

          if(AstNodeInfo.isConfigurationHtmlComment(bodyEntry)) {
            // remove the entry
            node.body.splice(i, 1);
          }
        }
      }
    });

    return ast;
  };

  return RemoveConfigurationHtmlCommentsPlugin;
};
<|endoftext|>


<commit_before>import React from 'react';
import { Link as AppLink, graphql } from 'gatsby';
import get from 'lodash/get';
import { Flex, Box, Text, Link } from 'rebass';
import Layout from '../components/Layout';
import SEO from '../components/SEO';
import { formatReadingTime, getGreeting } from '../utils';

const Section = props => (
	<Flex as="section" mt={5} flexWrap="wrap" {...props} />
);

const ListTitle = ({ children }) => (
	<Box as="header" width={[1, 1 / 4, 1, 1 / 4]}>
		<Text as="h3" m={0} fontSize={2}>
			{children}
		</Text>
	</Box>
);

const List = ({ children, ...props }) => (
	<Box
		as="ul"
		m={0}
		p={0}
		mt={[2, 0, 2, 0]}
		width={[1, 3 / 4, 1, 3 / 4]}
		role="list"
	>
		{children}
	</Box>
);

const ListItem = ({ children, indented = false, ...props }) => (
	<Text
		as="li"
		role="listitem"
		css={{ listStyle: 'none' }}
		pl={indented ? 3 : 0}
		mt={2}
		{...props}
	>
		{children}
	</Text>
);

const Separator = () => (
	<Text as="span" color="lightgray">
		/
	</Text>
);

const greeting = getGreeting();

const BlogIndex = ({ data }) => {
	const siteTitle = get(data, 'site.siteMetadata.title');
	const posts = get(data, 'allMarkdownRemark.edges');

	return (
		<Layout title={siteTitle}>
			<SEO />
			<Flex
				px={[4, 5, 5, 6]}
				pt={[5, 5, 5, 6]}
				flexWrap="wrap"
				justifyContent="flex-end"
			>
				<Box
					as="header"
					pr={[0, 0, 5, 5]}
					mt={[0, 0, 3, 0]}
					width={[1, 1, 1 / 2, 1 / 2]}
				>
					<Text as="h1" m={0} fontSize={2} fontWeight="normal">
						Hey there, I'm Pete.
					</Text>
					<Text as="h2" m={0} mt={2} fontSize={2} fontWeight="normal">
						{greeting}
					</Text>
					<Text as="small" color="gray" mt={3} css={{ display: 'block' }}>
						Available from Aug 2019.
					</Text>
				</Box>
				<Box mt={[5, 5, 3, 0]} width={[1, 1, 1 / 2, 1 / 2]}>

						<ListTitle>Work</ListTitle>
						<List>
							<ListItem mt={0}>
								<Link href="https://fath.om">Fathom London</Link>
							</ListItem>
							<ListItem indented>
								<Link href="https://commerzbank.com">Commerzbank</Link>
							</ListItem>
							<ListItem indented>
								<Link href="https://alixpartners.com">AlixPartners</Link>
							</ListItem>
							<ListItem indented>
								<Link href="https://accessfintech.com">Access Fintech</Link>
							</ListItem>
							<ListItem indented>
								<Link href="https://aimia.com">Aimia Inc</Link>
							</ListItem>
							<ListItem indented>
								<Link href="https://gsma.com">GSMA</Link>
							</ListItem>
							<ListItem>
								<Link href="https://barclays.co.uk">Barclays</Link>
							</ListItem>
							<ListItem>
								<Link href="https://www.seymourpowell.com">Seymourpowell</Link>
							</ListItem>
							<ListItem>
								<Link href="http://umbrellium.co.uk">Umbrellium</Link>
							</ListItem>
							<ListItem indented>
								<Link href="https://www.thingful.net">Thingful</Link>
							</ListItem>
							<ListItem indented>
								<Link href="https://youtube.com/watch?v=MjoumVT070A">
									Burble Paris <Separator /> Samsung
								</Link>
							</ListItem>
							<ListItem>
								<Link href="https://xively.com">
									Xively <Separator /> Google
								</Link>
							</ListItem>
							<ListItem>
								<Link href="https://www.telecom.pt/en-us">
									Portugal Telecom
								</Link>
							</ListItem>
							<ListItem>
								<Link color="gray" href="https://linkedin.com/in/petecorreia/">
									More →
								</Link>
							</ListItem>
						</List>

						<ListTitle>Writing</ListTitle>
						<List>
							{posts.map(({ node }, index) => {
								const title =
									get(node, 'frontmatter.title') || node.fields.slug;
								return (
									<ListItem key={node.fields.slug} mt={!index ? 0 : 3}>
										<AppLink to={node.fields.slug} css={{ lineHeight: 1.4 }}>
											{title}
										</AppLink>
										<Text
											as="small"
											color="gray"
											mt={1}
											css={{ display: 'block' }}
										>
											{node.frontmatter.date}
											{` — ${formatReadingTime(node.timeToRead)}`}
										</Text>
									</ListItem>
								);
							})}
						</List>

						<ListTitle>Stack</ListTitle>
						<List>
							<ListItem mt={0}>React</ListItem>
							<ListItem>Typescript</ListItem>
							<ListItem>Styled Components</ListItem>
							<ListItem>
								Dataviz <Separator /> D3
							</ListItem>
							<ListItem>GraphQL</ListItem>
							<ListItem>Design Systems</ListItem>
						</List>

						<ListTitle>OSS</ListTitle>
						<List>
							<ListItem mt={0}>
								<Link href="https://react-circular-input.now.sh/">
									react-circular-input
								</Link>
							</ListItem>
							<ListItem>
								<Link href="https://tsx-docs.now.sh/">tsx-docs</Link>
							</ListItem>
							<ListItem>
								<Link href="https://github.com/petecorreia/analyze-cra-bundle">
									analyze-cra-bundle
								</Link>
							</ListItem>
						</List>

						<ListTitle>Links</ListTitle>
						<List>
							<ListItem mt={0}>
								<Link href="https://twitter.com/petecorreia">Twitter</Link>
							</ListItem>
							<ListItem>
								<Link href="https://github.com/petecorreia">Github</Link>
							</ListItem>
							<ListItem>
								<Link href="https://linkedin.com/in/petecorreia/">
									LinkedIn
								</Link>
							</ListItem>
							<ListItem>
								<Link href="/rss.xml">RSS</Link>
							</ListItem>
						</List>

				</Box>
			</Flex>
		</Layout>
	);
};

export default BlogIndex;

export const pageQuery = graphql`
	query {
		site {
			siteMetadata {
				title
				description
			}
		}
		allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
			edges {
				node {
					fields {
						slug
					}
					timeToRead
					frontmatter {
						date(formatString: "MMMM DD, YYYY")
						title
						spoiler
					}
				}
			}
		}
	}
`;
<commit_msg>Move up OSS section<commit_after>import React from 'react';
import { Link as AppLink, graphql } from 'gatsby';
import get from 'lodash/get';
import { Flex, Box, Text, Link } from 'rebass';
import Layout from '../components/Layout';
import SEO from '../components/SEO';
import { formatReadingTime, getGreeting } from '../utils';

const Section = props => (
	<Flex as="section" mt={5} flexWrap="wrap" {...props} />
);

const ListTitle = ({ children }) => (
	<Box as="header" width={[1, 1 / 4, 1, 1 / 4]}>
		<Text as="h3" m={0} fontSize={2}>
			{children}
		</Text>
	</Box>
);

const List = ({ children, ...props }) => (
	<Box
		as="ul"
		m={0}
		p={0}
		mt={[2, 0, 2, 0]}
		width={[1, 3 / 4, 1, 3 / 4]}
		role="list"
	>
		{children}
	</Box>
);

const ListItem = ({ children, indented = false, ...props }) => (
	<Text
		as="li"
		role="listitem"
		css={{ listStyle: 'none' }}
		pl={indented ? 3 : 0}
		mt={2}
		{...props}
	>
		{children}
	</Text>
);

const Separator = () => (
	<Text as="span" color="lightgray">
		/
	</Text>
);

const greeting = getGreeting();

const BlogIndex = ({ data }) => {
	const siteTitle = get(data, 'site.siteMetadata.title');
	const posts = get(data, 'allMarkdownRemark.edges');

	return (
		<Layout title={siteTitle}>
			<SEO />
			<Flex
				px={[4, 5, 5, 6]}
				pt={[5, 5, 5, 6]}
				flexWrap="wrap"
				justifyContent="flex-end"
			>
				<Box
					as="header"
					pr={[0, 0, 5, 5]}
					mt={[0, 0, 3, 0]}
					width={[1, 1, 1 / 2, 1 / 2]}
				>
					<Text as="h1" m={0} fontSize={2} fontWeight="normal">
						Hey there, I'm Pete.
					</Text>
					<Text as="h2" m={0} mt={2} fontSize={2} fontWeight="normal">
						{greeting}
					</Text>
					<Text as="small" color="gray" mt={3} css={{ display: 'block' }}>
						Available from Aug 2019.
					</Text>
				</Box>
				<Box mt={[5, 5, 3, 0]} width={[1, 1, 1 / 2, 1 / 2]}>

						<ListTitle>Work</ListTitle>
						<List>
							<ListItem mt={0}>
								<Link href="https://fath.om">Fathom London</Link>
							</ListItem>
							<ListItem indented>
								<Link href="https://commerzbank.com">Commerzbank</Link>
							</ListItem>
							<ListItem indented>
								<Link href="https://alixpartners.com">AlixPartners</Link>
							</ListItem>
							<ListItem indented>
								<Link href="https://accessfintech.com">Access Fintech</Link>
							</ListItem>
							<ListItem indented>
								<Link href="https://aimia.com">Aimia Inc</Link>
							</ListItem>
							<ListItem indented>
								<Link href="https://gsma.com">GSMA</Link>
							</ListItem>
							<ListItem>
								<Link href="https://barclays.co.uk">Barclays</Link>
							</ListItem>
							<ListItem>
								<Link href="https://www.seymourpowell.com">Seymourpowell</Link>
							</ListItem>
							<ListItem>
								<Link href="http://umbrellium.co.uk">Umbrellium</Link>
							</ListItem>
							<ListItem indented>
								<Link href="https://www.thingful.net">Thingful</Link>
							</ListItem>
							<ListItem indented>
								<Link href="https://youtube.com/watch?v=MjoumVT070A">
									Burble Paris <Separator /> Samsung
								</Link>
							</ListItem>
							<ListItem>
								<Link href="https://xively.com">
									Xively <Separator /> Google
								</Link>
							</ListItem>
							<ListItem>
								<Link href="https://www.telecom.pt/en-us">
									Portugal Telecom
								</Link>
							</ListItem>
							<ListItem>
								<Link color="gray" href="https://linkedin.com/in/petecorreia/">
									More →
								</Link>
							</ListItem>
						</List>

						<ListTitle>OSS</ListTitle>
						<List>
							<ListItem mt={0}>
								<Link href="https://react-circular-input.now.sh/">
									react-circular-input
								</Link>
							</ListItem>
							<ListItem>
								<Link href="https://tsx-docs.now.sh/">tsx-docs</Link>
							</ListItem>
							<ListItem>
								<Link href="https://github.com/petecorreia/analyze-cra-bundle">
									analyze-cra-bundle
								</Link>
							</ListItem>
						</List>

						<ListTitle>Writing</ListTitle>
						<List>
							{posts.map(({ node }, index) => {
								const title =
									get(node, 'frontmatter.title') || node.fields.slug;
								return (
									<ListItem key={node.fields.slug} mt={!index ? 0 : 3}>
										<AppLink to={node.fields.slug} css={{ lineHeight: 1.4 }}>
											{title}
										</AppLink>
										<Text
											as="small"
											color="gray"
											mt={1}
											css={{ display: 'block' }}
										>
											{node.frontmatter.date}
											{` — ${formatReadingTime(node.timeToRead)}`}
										</Text>
									</ListItem>
								);
							})}
						</List>

						<ListTitle>Stack</ListTitle>
						<List>
							<ListItem mt={0}>React</ListItem>
							<ListItem>Typescript</ListItem>
							<ListItem>Styled Components</ListItem>
							<ListItem>
								Dataviz <Separator /> D3
							</ListItem>
							<ListItem>GraphQL</ListItem>
							<ListItem>Design Systems</ListItem>
						</List>

						<ListTitle>Links</ListTitle>
						<List>
							<ListItem mt={0}>
								<Link href="https://twitter.com/petecorreia">Twitter</Link>
							</ListItem>
							<ListItem>
								<Link href="https://github.com/petecorreia">Github</Link>
							</ListItem>
							<ListItem>
								<Link href="https://linkedin.com/in/petecorreia/">
									LinkedIn
								</Link>
							</ListItem>
							<ListItem>
								<Link href="/rss.xml">RSS</Link>
							</ListItem>
						</List>

				</Box>
			</Flex>
		</Layout>
	);
};

export default BlogIndex;

export const pageQuery = graphql`
	query {
		site {
			siteMetadata {
				title
				description
			}
		}
		allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
			edges {
				node {
					fields {
						slug
					}
					timeToRead
					frontmatter {
						date(formatString: "MMMM DD, YYYY")
						title
						spoiler
					}
				}
			}
		}
	}
`;
<|endoftext|>