<commit_before>import api, { getLinks } from '../api';
import IntlMessageFormat from 'intl-messageformat';
import { fetchFollowRequests, fetchRelationships } from './accounts';
import {
  importFetchedAccount,
  importFetchedAccounts,
  importFetchedStatus,
  importFetchedStatuses,
} from './importer';
import { submitMarkers } from './markers';
import { saveSettings } from './settings';
import { defineMessages } from 'react-intl';
import { List as ImmutableList } from 'immutable';
import { unescapeHTML } from '../utils/html';
import { getFiltersRegex } from '../selectors';
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
import compareId from 'mastodon/compare_id';
import { searchTextFromRawStatus } from 'mastodon/actions/importer/normalizer';
import { requestNotificationPermission } from '../utils/notifications';

export const NOTIFICATIONS_UPDATE      = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';

export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
export const NOTIFICATIONS_EXPAND_FAIL    = 'NOTIFICATIONS_EXPAND_FAIL';

export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';

export const NOTIFICATIONS_CLEAR        = 'NOTIFICATIONS_CLEAR';
export const NOTIFICATIONS_SCROLL_TOP   = 'NOTIFICATIONS_SCROLL_TOP';
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';

export const NOTIFICATIONS_MOUNT   = 'NOTIFICATIONS_MOUNT';
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';

export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';

export const NOTIFICATIONS_SET_BROWSER_SUPPORT    = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';

defineMessages({
  mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
  group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
});

const fetchRelatedRelationships = (dispatch, notifications) => {
  const accountIds = notifications.filter(item => ['follow', 'follow_request', 'admin.sign_up'].indexOf(item.type) !== -1).map(item => item.account.id);

  if (accountIds.length > 0) {
    dispatch(fetchRelationships(accountIds));
  }
};

export const loadPending = () => ({
  type: NOTIFICATIONS_LOAD_PENDING,
});

export function updateNotifications(notification, intlMessages, intlLocale) {
  return (dispatch, getState) => {
    const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
    const showAlert    = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
    const playSound    = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
    const filters      = getFiltersRegex(getState(), { contextType: 'notifications' });

    let filtered = false;

    if (['mention', 'status'].includes(notification.type)) {
      const dropRegex   = filters[0];
      const regex       = filters[1];
      const searchIndex = searchTextFromRawStatus(notification.status);

      if (dropRegex && dropRegex.test(searchIndex)) {
        return;
      }

      filtered = regex && regex.test(searchIndex);
    }

    if (['follow_request'].includes(notification.type)) {
      dispatch(fetchFollowRequests());
    }

    dispatch(submitMarkers());

    if (showInColumn) {
      dispatch(importFetchedAccount(notification.account));

      if (notification.status) {
        dispatch(importFetchedStatus(notification.status));
      }

      dispatch({
        type: NOTIFICATIONS_UPDATE,
        notification,
        usePendingItems: preferPendingItems,
        meta: (playSound && !filtered) ? { sound: 'boop' } : undefined,
      });

      fetchRelatedRelationships(dispatch, [notification]);
    } else if (playSound && !filtered) {
      dispatch({
        type: NOTIFICATIONS_UPDATE_NOOP,
        meta: { sound: 'boop' },
      });
    }

    // Desktop notifications
    if (typeof window.Notification !== 'undefined' && showAlert && !filtered) {
      const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
      const body  = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');

      const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });

      notify.addEventListener('click', () => {
        window.focus();
        notify.close();
      });
    }
  };
};

const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();

const excludeTypesFromFilter = filter => {
  const allTypes = ImmutableList([
    'follow',
    'follow_request',
    'favourite',
    'reblog',
    'mention',
    'poll',
    'status',
    'update',
    'admin.sign_up',
  ]);

  return allTypes.filterNot(item => item === filter).toJS();
};

const noOp = () => {};

export function expandNotifications({ maxId } = {}, done = noOp) {
  return (dispatch, getState) => {
    const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
    const notifications = getState().get('notifications');
    const isLoadingMore = !!maxId;

    if (notifications.get('isLoading')) {
      done();
      return;
    }

    const params = {
      max_id: maxId,
      exclude_types: activeFilter === 'all'
        ? excludeTypesFromSettings(getState())
        : excludeTypesFromFilter(activeFilter),
    };

    if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) {
      const a = notifications.getIn(['pendingItems', 0, 'id']);
      const b = notifications.getIn(['items', 0, 'id']);

      if (a && b && compareId(a, b) > 0) {
        params.since_id = a;
      } else {
        params.since_id = b || a;
      }
    }

    const isLoadingRecent = !!params.since_id;

    dispatch(expandNotificationsRequest(isLoadingMore));

    api(getState).get('/api/v1/notifications', { params }).then(response => {
      const next = getLinks(response).refs.find(link => link.rel === 'next');

      dispatch(importFetchedAccounts(response.data.map(item => item.account)));
      dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));

      dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
      fetchRelatedRelationships(dispatch, response.data);
      dispatch(submitMarkers());
    }).catch(error => {
      dispatch(expandNotificationsFail(error, isLoadingMore));
    }).finally(() => {
      done();
    });
  };
};

export function expandNotificationsRequest(isLoadingMore) {
  return {
    type: NOTIFICATIONS_EXPAND_REQUEST,
    skipLoading: !isLoadingMore,
  };
};

export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) {
  return {
    type: NOTIFICATIONS_EXPAND_SUCCESS,
    notifications,
    next,
    isLoadingRecent: isLoadingRecent,
    usePendingItems,
    skipLoading: !isLoadingMore,
  };
};

export function expandNotificationsFail(error, isLoadingMore) {
  return {
    type: NOTIFICATIONS_EXPAND_FAIL,
    error,
    skipLoading: !isLoadingMore,
    skipAlert: !isLoadingMore,
  };
};

export function clearNotifications() {
  return (dispatch, getState) => {
    dispatch({
      type: NOTIFICATIONS_CLEAR,
    });

    api(getState).post('/api/v1/notifications/clear');
  };
};

export function scrollTopNotifications(top) {
  return {
    type: NOTIFICATIONS_SCROLL_TOP,
    top,
  };
};

export function setFilter (filterType) {
  return dispatch => {
    dispatch({
      type: NOTIFICATIONS_FILTER_SET,
      path: ['notifications', 'quickFilter', 'active'],
      value: filterType,
    });
    dispatch(expandNotifications());
    dispatch(saveSettings());
  };
};

export const mountNotifications = () => ({
  type: NOTIFICATIONS_MOUNT,
});

export const unmountNotifications = () => ({
  type: NOTIFICATIONS_UNMOUNT,
});

export const markNotificationsAsRead = () => ({
  type: NOTIFICATIONS_MARK_AS_READ,
});

// Browser support
export function setupBrowserNotifications() {
  return dispatch => {
    dispatch(setBrowserSupport('Notification' in window));
    if ('Notification' in window) {
      dispatch(setBrowserPermission(Notification.permission));
    }

    if ('Notification' in window && 'permissions' in navigator) {
      navigator.permissions.query({ name: 'notifications' }).then((status) => {
        status.onchange = () => dispatch(setBrowserPermission(Notification.permission));
      }).catch(console.warn);
    }
  };
}

export function requestBrowserPermission(callback = noOp) {
  return dispatch => {
    requestNotificationPermission((permission) => {
      dispatch(setBrowserPermission(permission));
      callback(permission);
    });
  };
};

export function setBrowserSupport (value) {
  return {
    type: NOTIFICATIONS_SET_BROWSER_SUPPORT,
    value,
  };
}

export function setBrowserPermission (value) {
  return {
    type: NOTIFICATIONS_SET_BROWSER_PERMISSION,
    value,
  };
}
<commit_msg>Fix streaming notifications not using quick filter logic (#18316)<commit_after>import api, { getLinks } from '../api';
import IntlMessageFormat from 'intl-messageformat';
import { fetchFollowRequests, fetchRelationships } from './accounts';
import {
  importFetchedAccount,
  importFetchedAccounts,
  importFetchedStatus,
  importFetchedStatuses,
} from './importer';
import { submitMarkers } from './markers';
import { saveSettings } from './settings';
import { defineMessages } from 'react-intl';
import { List as ImmutableList } from 'immutable';
import { unescapeHTML } from '../utils/html';
import { getFiltersRegex } from '../selectors';
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
import compareId from 'mastodon/compare_id';
import { searchTextFromRawStatus } from 'mastodon/actions/importer/normalizer';
import { requestNotificationPermission } from '../utils/notifications';

export const NOTIFICATIONS_UPDATE      = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';

export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
export const NOTIFICATIONS_EXPAND_FAIL    = 'NOTIFICATIONS_EXPAND_FAIL';

export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';

export const NOTIFICATIONS_CLEAR        = 'NOTIFICATIONS_CLEAR';
export const NOTIFICATIONS_SCROLL_TOP   = 'NOTIFICATIONS_SCROLL_TOP';
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';

export const NOTIFICATIONS_MOUNT   = 'NOTIFICATIONS_MOUNT';
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';

export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';

export const NOTIFICATIONS_SET_BROWSER_SUPPORT    = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';

defineMessages({
  mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
  group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
});

const fetchRelatedRelationships = (dispatch, notifications) => {
  const accountIds = notifications.filter(item => ['follow', 'follow_request', 'admin.sign_up'].indexOf(item.type) !== -1).map(item => item.account.id);

  if (accountIds.length > 0) {
    dispatch(fetchRelationships(accountIds));
  }
};

export const loadPending = () => ({
  type: NOTIFICATIONS_LOAD_PENDING,
});

export function updateNotifications(notification, intlMessages, intlLocale) {
  return (dispatch, getState) => {
    const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
    const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type;
    const showAlert    = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
    const playSound    = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
    const filters      = getFiltersRegex(getState(), { contextType: 'notifications' });

    let filtered = false;

    if (['mention', 'status'].includes(notification.type)) {
      const dropRegex   = filters[0];
      const regex       = filters[1];
      const searchIndex = searchTextFromRawStatus(notification.status);

      if (dropRegex && dropRegex.test(searchIndex)) {
        return;
      }

      filtered = regex && regex.test(searchIndex);
    }

    if (['follow_request'].includes(notification.type)) {
      dispatch(fetchFollowRequests());
    }

    dispatch(submitMarkers());

    if (showInColumn) {
      dispatch(importFetchedAccount(notification.account));

      if (notification.status) {
        dispatch(importFetchedStatus(notification.status));
      }

      dispatch({
        type: NOTIFICATIONS_UPDATE,
        notification,
        usePendingItems: preferPendingItems,
        meta: (playSound && !filtered) ? { sound: 'boop' } : undefined,
      });

      fetchRelatedRelationships(dispatch, [notification]);
    } else if (playSound && !filtered) {
      dispatch({
        type: NOTIFICATIONS_UPDATE_NOOP,
        meta: { sound: 'boop' },
      });
    }

    // Desktop notifications
    if (typeof window.Notification !== 'undefined' && showAlert && !filtered) {
      const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
      const body  = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');

      const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });

      notify.addEventListener('click', () => {
        window.focus();
        notify.close();
      });
    }
  };
};

const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();

const excludeTypesFromFilter = filter => {
  const allTypes = ImmutableList([
    'follow',
    'follow_request',
    'favourite',
    'reblog',
    'mention',
    'poll',
    'status',
    'update',
    'admin.sign_up',
  ]);

  return allTypes.filterNot(item => item === filter).toJS();
};

const noOp = () => {};

export function expandNotifications({ maxId } = {}, done = noOp) {
  return (dispatch, getState) => {
    const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
    const notifications = getState().get('notifications');
    const isLoadingMore = !!maxId;

    if (notifications.get('isLoading')) {
      done();
      return;
    }

    const params = {
      max_id: maxId,
      exclude_types: activeFilter === 'all'
        ? excludeTypesFromSettings(getState())
        : excludeTypesFromFilter(activeFilter),
    };

    if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) {
      const a = notifications.getIn(['pendingItems', 0, 'id']);
      const b = notifications.getIn(['items', 0, 'id']);

      if (a && b && compareId(a, b) > 0) {
        params.since_id = a;
      } else {
        params.since_id = b || a;
      }
    }

    const isLoadingRecent = !!params.since_id;

    dispatch(expandNotificationsRequest(isLoadingMore));

    api(getState).get('/api/v1/notifications', { params }).then(response => {
      const next = getLinks(response).refs.find(link => link.rel === 'next');

      dispatch(importFetchedAccounts(response.data.map(item => item.account)));
      dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));

      dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
      fetchRelatedRelationships(dispatch, response.data);
      dispatch(submitMarkers());
    }).catch(error => {
      dispatch(expandNotificationsFail(error, isLoadingMore));
    }).finally(() => {
      done();
    });
  };
};

export function expandNotificationsRequest(isLoadingMore) {
  return {
    type: NOTIFICATIONS_EXPAND_REQUEST,
    skipLoading: !isLoadingMore,
  };
};

export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) {
  return {
    type: NOTIFICATIONS_EXPAND_SUCCESS,
    notifications,
    next,
    isLoadingRecent: isLoadingRecent,
    usePendingItems,
    skipLoading: !isLoadingMore,
  };
};

export function expandNotificationsFail(error, isLoadingMore) {
  return {
    type: NOTIFICATIONS_EXPAND_FAIL,
    error,
    skipLoading: !isLoadingMore,
    skipAlert: !isLoadingMore,
  };
};

export function clearNotifications() {
  return (dispatch, getState) => {
    dispatch({
      type: NOTIFICATIONS_CLEAR,
    });

    api(getState).post('/api/v1/notifications/clear');
  };
};

export function scrollTopNotifications(top) {
  return {
    type: NOTIFICATIONS_SCROLL_TOP,
    top,
  };
};

export function setFilter (filterType) {
  return dispatch => {
    dispatch({
      type: NOTIFICATIONS_FILTER_SET,
      path: ['notifications', 'quickFilter', 'active'],
      value: filterType,
    });
    dispatch(expandNotifications());
    dispatch(saveSettings());
  };
};

export const mountNotifications = () => ({
  type: NOTIFICATIONS_MOUNT,
});

export const unmountNotifications = () => ({
  type: NOTIFICATIONS_UNMOUNT,
});

export const markNotificationsAsRead = () => ({
  type: NOTIFICATIONS_MARK_AS_READ,
});

// Browser support
export function setupBrowserNotifications() {
  return dispatch => {
    dispatch(setBrowserSupport('Notification' in window));
    if ('Notification' in window) {
      dispatch(setBrowserPermission(Notification.permission));
    }

    if ('Notification' in window && 'permissions' in navigator) {
      navigator.permissions.query({ name: 'notifications' }).then((status) => {
        status.onchange = () => dispatch(setBrowserPermission(Notification.permission));
      }).catch(console.warn);
    }
  };
}

export function requestBrowserPermission(callback = noOp) {
  return dispatch => {
    requestNotificationPermission((permission) => {
      dispatch(setBrowserPermission(permission));
      callback(permission);
    });
  };
};

export function setBrowserSupport (value) {
  return {
    type: NOTIFICATIONS_SET_BROWSER_SUPPORT,
    value,
  };
}

export function setBrowserPermission (value) {
  return {
    type: NOTIFICATIONS_SET_BROWSER_PERMISSION,
    value,
  };
}
<|endoftext|>


<commit_before>module.exports = BboxAlignedText;

/**
 * Creates a new BboxAlignedText object - a text object that can be drawn with
 * anchor points based on a tight bounding box around the text.
 * @constructor
 * @param {object} font               p5.Font object
 * @param {string} text               String to display
 * @param {number} [fontSize=12]      Font size to use for string
 * @param {object} [pInstance=window] Reference to p5 instance, leave blank if
 *                                    sketch is global
 * @example
 * var font, bboxText;
 * function preload() {
 *     font = loadFont("./assets/Regular.ttf");
 * }
 * function setup() {
 *     createCanvas(400, 600);
 *     background(0);
 *
 *     bboxText = new BboxAlignedText(font, "Hey!", 30);
 *     bboxText.setRotation(PI / 4);
 *     bboxText.setAnchor(BboxAlignedText.ALIGN.BOX_CENTER,
 *                        BboxAlignedText.BASELINE.BOX_CENTER);
 *
 *     fill("#00A8EA");
 *     noStroke();
 *     bboxText.draw(width / 2, height / 2, true);
 * }
 */
function BboxAlignedText(font, text, fontSize, pInstance) {
    this._font = font;
    this._text = text;
    this._fontSize = (fontSize !== undefined) ? fontSize : 12;
    this.p = pInstance || window; // If instance is omitted, assume global scope
    this._rotation = 0;
    this._hAlign = BboxAlignedText.ALIGN.BOX_CENTER;
    this._vAlign = BboxAlignedText.BASELINE.BOX_CENTER;
    this._calculateMetrics(true);
}

/**
 * Vertical alignment values
 * @public
 * @static
 * @readonly
 * @enum {string}
 */
BboxAlignedText.ALIGN = {
    /** Draw from the left of the bbox */
    BOX_LEFT: "box_left",
    /** Draw from the center of the bbox */
    BOX_CENTER: "box_center",
    /** Draw from the right of the bbox */
    BOX_RIGHT: "box_right"
};

/**
 * Baseline alignment values
 * @public
 * @static
 * @readonly
 * @enum {string}
 */
BboxAlignedText.BASELINE = {
    /** Draw from the top of the bbox */
    BOX_TOP: "box_top",
    /** Draw from the center of the bbox */
    BOX_CENTER: "box_center",
    /** Draw from the bottom of the bbox */
    BOX_BOTTOM: "box_bottom",
    /**
     * Draw from half the height of the font. Specifically the height is
     * calculated as: ascent + descent.
     */
    FONT_CENTER: "font_center",
    /** Draw from the the normal font baseline */
    ALPHABETIC: "alphabetic"
};

/**
 * Set current text
 * @public
 * @param {string} string Text string to display
 */
BboxAlignedText.prototype.setText = function(string) {
    this._text = string;
    this._calculateMetrics(false);
};

/**
 * Set current text size
 * @public
 * @param {number} fontSize Text size
 */
BboxAlignedText.prototype.setTextSize = function(fontSize) {
    this._fontSize = fontSize;
    this._calculateMetrics(true);
};

/**
 * Set rotation of text
 * @public
 * @param {number} angle Rotation in radians
 */
BboxAlignedText.prototype.setRotation = function(angle) {
    this._rotation = angle;
};

/**
 * Set anchor point for text (horizonal and vertical alignment) relative to
 * bounding box
 * @public
 * @param {string} [hAlign=CENTER] Horizonal alignment
 * @param {string} [vAlign=CENTER] Vertical baseline
 */
BboxAlignedText.prototype.setAnchor = function(hAlign, vAlign) {
    this._hAlign = hAlign || BboxAlignedText.ALIGN.CENTER;
    this._vAlign = vAlign || BboxAlignedText.BASELINE.CENTER;
};

/**
 * Get the bounding box when the text is placed at the specified coordinates.
 * Note: this is the unrotated bounding box!
 * @param  {number} x X coordinate
 * @param  {number} y Y coordinate
 * @return {object}   Returns an object with properties: x, y, w, h
 */
BboxAlignedText.prototype.getBbox = function(x, y) {
    var pos = this._calculateAlignedCoords(x, y);
    return {
        x: pos.x + this._boundsOffset.x,
        y: pos.y + this._boundsOffset.y,
        w: this.width,
        h: this.height
    };
};

/**
 * Draws the text particle with the specified style parameters. Note: this is
 * going to set the textFont, textSize & rotation before drawing. You should set
 * the color/stroke/fill that you want before drawing. This function will clean
 * up after itself and reset styling back to what it was before it was called.
 * @public
 * @param  {number}  [x=0]              X coordinate of text anchor
 * @param  {number}  [y=0]              Y coordinate of text anchor
 * @param  {boolean} [drawBounds=false] Flag for drawing bounding box
 */
BboxAlignedText.prototype.draw = function(x, y, drawBounds) {
    drawBounds = drawBounds || false;
    var pos = {
        x: (x !== undefined) ? x : 0,
        y: (y !== undefined) ? y : 0
    };

    this.p.push();

        if (this._rotation) {
            pos = this._calculateRotatedCoords(pos.x, pos.y, this._rotation);
            this.p.rotate(this._rotation);
        }

        pos = this._calculateAlignedCoords(pos.x, pos.y);

        this.p.textAlign(this.p.LEFT, this.p.BASELINE);
        this.p.textFont(this._font);
        this.p.textSize(this._fontSize);
        this.p.text(this._text, pos.x, pos.y);

        if (drawBounds) {
            this.p.stroke(200);
            var boundsX = pos.x + this._boundsOffset.x;
            var boundsY = pos.y + this._boundsOffset.y;
            this.p.noFill();
            this.p.rect(boundsX, boundsY, this.width, this.height);
        }

    this.p.pop();
};

/**
 * Project the coordinates (x, y) into a rotated coordinate system
 * @private
 * @param  {number} x     X coordinate (in unrotated space)
 * @param  {number} y     Y coordinate (in unrotated space)
 * @param  {number} angle Radians of rotation to apply
 * @return {object}       Object with x & y properties
 */
BboxAlignedText.prototype._calculateRotatedCoords = function (x, y, angle) {
    var rx = Math.cos(angle) * x + Math.cos(Math.PI / 2 - angle) * y;
    var ry = -Math.sin(angle) * x + Math.sin(Math.PI / 2 - angle) * y;
    return {x: rx, y: ry};
};

/**
 * Calculates draw coordinates for the text, aligning based on the bounding box.
 * The text is eventually drawn with canvas alignment set to left & baseline, so
 * this function takes a desired pos & alignment and returns the appropriate
 * coordinates for the left & baseline.
 * @private
 * @param  {number} x      X coordinate
 * @param  {number} y      Y coordinate
 * @return {object}        Object with x & y properties
 */
BboxAlignedText.prototype._calculateAlignedCoords = function(x, y) {
    var newX, newY;
    switch (this._hAlign) {
        case BboxAlignedText.ALIGN.BOX_LEFT:
            newX = x;
            break;
        case BboxAlignedText.ALIGN.BOX_CENTER:
            newX = x - this.halfWidth;
            break;
        case BboxAlignedText.ALIGN.BOX_RIGHT:
            newX = x - this.width;
            break;
        default:
            newX = x;
            console.log("Unrecognized horizonal align:", this._hAlign);
            break;
    }
    switch (this._vAlign) {
        case BboxAlignedText.BASELINE.BOX_TOP:
            newY = y - this._boundsOffset.y;
            break;
        case BboxAlignedText.BASELINE.BOX_CENTER:
            newY = y + this._distBaseToMid;
            break;
        case BboxAlignedText.BASELINE.BOX_BOTTOM:
            newY = y - this._distBaseToBottom;
            break;
        case BboxAlignedText.BASELINE.FONT_CENTER:
            // Height is approximated as ascent + descent
            newY = y - this._descent + (this._ascent + this._descent) / 2;
            break;
        case BboxAlignedText.BASELINE.ALPHABETIC:
            newY = y;
            break;
        default:
            newY = y;
            console.log("Unrecognized vertical align:", this._vAlign);
            break;
    }
    return {x: newX, y: newY};
};

/**
 * Calculates bounding box and various metrics for the current text and font
 * @private
 */
BboxAlignedText.prototype._calculateMetrics = function(shouldUpdateHeight) {
    // p5 0.5.0 has a bug - text bounds are clipped by (0, 0)
    // Calculating bounds hack
    var bounds = this._font.textBounds(this._text, 1000, 1000, this._fontSize);
    // Bounds is a reference - if we mess with it directly, we can mess up
    // future values! (It changes the bbox cache in p5.)
    bounds = {
        x: bounds.x - 1000,
        y: bounds.y - 1000,
        w: bounds.w,
        h: bounds.h
    };

    if (shouldUpdateHeight) {
        this._ascent = this._font._textAscent(this._fontSize);
        this._descent = this._font._textDescent(this._fontSize);
    }

    // Use bounds to calculate font metrics
    this.width = bounds.w;
    this.height = bounds.h;
    this.halfWidth = this.width / 2;
    this.halfHeight = this.height / 2;
    this._boundsOffset = { x: bounds.x, y: bounds.y };
    this._distBaseToMid = Math.abs(bounds.y) - this.halfHeight;
    this._distBaseToBottom = this.height - Math.abs(bounds.y);
};<commit_msg>Extend the library to have a method for calculating the points along a text path<commit_after>module.exports = BboxAlignedText;

/**
 * Creates a new BboxAlignedText object - a text object that can be drawn with
 * anchor points based on a tight bounding box around the text.
 * @constructor
 * @param {object} font               p5.Font object
 * @param {string} text               String to display
 * @param {number} [fontSize=12]      Font size to use for string
 * @param {object} [pInstance=window] Reference to p5 instance, leave blank if
 *                                    sketch is global
 * @example
 * var font, bboxText;
 * function preload() {
 *     font = loadFont("./assets/Regular.ttf");
 * }
 * function setup() {
 *     createCanvas(400, 600);
 *     background(0);
 *
 *     bboxText = new BboxAlignedText(font, "Hey!", 30);
 *     bboxText.setRotation(PI / 4);
 *     bboxText.setAnchor(BboxAlignedText.ALIGN.BOX_CENTER,
 *                        BboxAlignedText.BASELINE.BOX_CENTER);
 *
 *     fill("#00A8EA");
 *     noStroke();
 *     bboxText.draw(width / 2, height / 2, true);
 * }
 */
function BboxAlignedText(font, text, fontSize, pInstance) {
    this._font = font;
    this._text = text;
    this._fontSize = (fontSize !== undefined) ? fontSize : 12;
    this.p = pInstance || window; // If instance is omitted, assume global scope
    this._rotation = 0;
    this._hAlign = BboxAlignedText.ALIGN.BOX_CENTER;
    this._vAlign = BboxAlignedText.BASELINE.BOX_CENTER;
    this._calculateMetrics(true);
}

/**
 * Vertical alignment values
 * @public
 * @static
 * @readonly
 * @enum {string}
 */
BboxAlignedText.ALIGN = {
    /** Draw from the left of the bbox */
    BOX_LEFT: "box_left",
    /** Draw from the center of the bbox */
    BOX_CENTER: "box_center",
    /** Draw from the right of the bbox */
    BOX_RIGHT: "box_right"
};

/**
 * Baseline alignment values
 * @public
 * @static
 * @readonly
 * @enum {string}
 */
BboxAlignedText.BASELINE = {
    /** Draw from the top of the bbox */
    BOX_TOP: "box_top",
    /** Draw from the center of the bbox */
    BOX_CENTER: "box_center",
    /** Draw from the bottom of the bbox */
    BOX_BOTTOM: "box_bottom",
    /**
     * Draw from half the height of the font. Specifically the height is
     * calculated as: ascent + descent.
     */
    FONT_CENTER: "font_center",
    /** Draw from the the normal font baseline */
    ALPHABETIC: "alphabetic"
};

/**
 * Set current text
 * @public
 * @param {string} string Text string to display
 */
BboxAlignedText.prototype.setText = function(string) {
    this._text = string;
    this._calculateMetrics(false);
};

/**
 * Set current text size
 * @public
 * @param {number} fontSize Text size
 */
BboxAlignedText.prototype.setTextSize = function(fontSize) {
    this._fontSize = fontSize;
    this._calculateMetrics(true);
};

/**
 * Set rotation of text
 * @public
 * @param {number} angle Rotation in radians
 */
BboxAlignedText.prototype.setRotation = function(angle) {
    this._rotation = angle;
};

/**
 * Set anchor point for text (horizonal and vertical alignment) relative to
 * bounding box
 * @public
 * @param {string} [hAlign=CENTER] Horizonal alignment
 * @param {string} [vAlign=CENTER] Vertical baseline
 */
BboxAlignedText.prototype.setAnchor = function(hAlign, vAlign) {
    this._hAlign = hAlign || BboxAlignedText.ALIGN.CENTER;
    this._vAlign = vAlign || BboxAlignedText.BASELINE.CENTER;
};

/**
 * Get the bounding box when the text is placed at the specified coordinates.
 * Note: this is the unrotated bounding box! TODO: Fix this.
 * @param  {number} x X coordinate
 * @param  {number} y Y coordinate
 * @return {object}   Returns an object with properties: x, y, w, h
 */
BboxAlignedText.prototype.getBbox = function(x, y) {
    var pos = this._calculateAlignedCoords(x, y);
    return {
        x: pos.x + this._boundsOffset.x,
        y: pos.y + this._boundsOffset.y,
        w: this.width,
        h: this.height
    };
};

/**
 * Get an array of points that follow along the text path. This will take into
 * consideration the current alignment settings.
 * Note: this is a thin wrapper around a p5 method and doesn't handle unrotated
 * text! TODO: Fix this.
 * @param  {number} x         X coordinate
 * @param  {number} y         Y coordinate
 * @param  {object} [options] An object that can have:
 *                            - sampleFactor: ratio of path-length to number of
 *                              samples (default=0.25). Higher values yield more
 *                              points and are therefore more precise.
 *                            - simplifyThreshold: if set to a non-zero value,
 *                              collinear points will be removed. The value
 *                              represents the threshold angle to use when
 *                              determining whether two edges are collinear.
 * @return {array} An array of points, each with x, y & alpha (the path angle)
 */
BboxAlignedText.prototype.getTextPoints = function(x, y, options) {
    var points = this._font.textToPoints(this._text, x, y, this._fontSize,
                                         options);
    for (var i = 0; i < points.length; i += 1) {
        var pos = this._calculateAlignedCoords(points[i].x, points[i].y);
        points[i].x = pos.x;
        points[i].y = pos.y;
    }
    return points;
};

/**
 * Draws the text particle with the specified style parameters. Note: this is
 * going to set the textFont, textSize & rotation before drawing. You should set
 * the color/stroke/fill that you want before drawing. This function will clean
 * up after itself and reset styling back to what it was before it was called.
 * @public
 * @param  {number}  [x=0]              X coordinate of text anchor
 * @param  {number}  [y=0]              Y coordinate of text anchor
 * @param  {boolean} [drawBounds=false] Flag for drawing bounding box
 */
BboxAlignedText.prototype.draw = function(x, y, drawBounds) {
    drawBounds = drawBounds || false;
    var pos = {
        x: (x !== undefined) ? x : 0,
        y: (y !== undefined) ? y : 0
    };

    this.p.push();

        if (this._rotation) {
            pos = this._calculateRotatedCoords(pos.x, pos.y, this._rotation);
            this.p.rotate(this._rotation);
        }

        pos = this._calculateAlignedCoords(pos.x, pos.y);

        this.p.textAlign(this.p.LEFT, this.p.BASELINE);
        this.p.textFont(this._font);
        this.p.textSize(this._fontSize);
        this.p.text(this._text, pos.x, pos.y);

        if (drawBounds) {
            this.p.stroke(200);
            var boundsX = pos.x + this._boundsOffset.x;
            var boundsY = pos.y + this._boundsOffset.y;
            this.p.noFill();
            this.p.rect(boundsX, boundsY, this.width, this.height);
        }

    this.p.pop();
};

/**
 * Project the coordinates (x, y) into a rotated coordinate system
 * @private
 * @param  {number} x     X coordinate (in unrotated space)
 * @param  {number} y     Y coordinate (in unrotated space)
 * @param  {number} angle Radians of rotation to apply
 * @return {object}       Object with x & y properties
 */
BboxAlignedText.prototype._calculateRotatedCoords = function (x, y, angle) {
    var rx = Math.cos(angle) * x + Math.cos(Math.PI / 2 - angle) * y;
    var ry = -Math.sin(angle) * x + Math.sin(Math.PI / 2 - angle) * y;
    return {x: rx, y: ry};
};

/**
 * Calculates draw coordinates for the text, aligning based on the bounding box.
 * The text is eventually drawn with canvas alignment set to left & baseline, so
 * this function takes a desired pos & alignment and returns the appropriate
 * coordinates for the left & baseline.
 * @private
 * @param  {number} x      X coordinate
 * @param  {number} y      Y coordinate
 * @return {object}        Object with x & y properties
 */
BboxAlignedText.prototype._calculateAlignedCoords = function(x, y) {
    var newX, newY;
    switch (this._hAlign) {
        case BboxAlignedText.ALIGN.BOX_LEFT:
            newX = x;
            break;
        case BboxAlignedText.ALIGN.BOX_CENTER:
            newX = x - this.halfWidth;
            break;
        case BboxAlignedText.ALIGN.BOX_RIGHT:
            newX = x - this.width;
            break;
        default:
            newX = x;
            console.log("Unrecognized horizonal align:", this._hAlign);
            break;
    }
    switch (this._vAlign) {
        case BboxAlignedText.BASELINE.BOX_TOP:
            newY = y - this._boundsOffset.y;
            break;
        case BboxAlignedText.BASELINE.BOX_CENTER:
            newY = y + this._distBaseToMid;
            break;
        case BboxAlignedText.BASELINE.BOX_BOTTOM:
            newY = y - this._distBaseToBottom;
            break;
        case BboxAlignedText.BASELINE.FONT_CENTER:
            // Height is approximated as ascent + descent
            newY = y - this._descent + (this._ascent + this._descent) / 2;
            break;
        case BboxAlignedText.BASELINE.ALPHABETIC:
            newY = y;
            break;
        default:
            newY = y;
            console.log("Unrecognized vertical align:", this._vAlign);
            break;
    }
    return {x: newX, y: newY};
};

/**
 * Calculates bounding box and various metrics for the current text and font
 * @private
 */
BboxAlignedText.prototype._calculateMetrics = function(shouldUpdateHeight) {
    // p5 0.5.0 has a bug - text bounds are clipped by (0, 0)
    // Calculating bounds hack
    var bounds = this._font.textBounds(this._text, 1000, 1000, this._fontSize);
    // Bounds is a reference - if we mess with it directly, we can mess up
    // future values! (It changes the bbox cache in p5.)
    bounds = {
        x: bounds.x - 1000,
        y: bounds.y - 1000,
        w: bounds.w,
        h: bounds.h
    };

    if (shouldUpdateHeight) {
        this._ascent = this._font._textAscent(this._fontSize);
        this._descent = this._font._textDescent(this._fontSize);
    }

    // Use bounds to calculate font metrics
    this.width = bounds.w;
    this.height = bounds.h;
    this.halfWidth = this.width / 2;
    this.halfHeight = this.height / 2;
    this._boundsOffset = { x: bounds.x, y: bounds.y };
    this._distBaseToMid = Math.abs(bounds.y) - this.halfHeight;
    this._distBaseToBottom = this.height - Math.abs(bounds.y);
};<|endoftext|>


<commit_before>import classNames from 'classnames';
import {Link} from 'react-router';
import mixin from 'reactjs-mixin';
/* eslint-disable no-unused-vars */
import React from 'react';
/* eslint-enable no-unused-vars */
import {StoreMixin} from 'mesosphere-shared-reactjs';
import {Table} from 'reactjs-components';

import FilterBar from '../../../components/FilterBar';
import FilterHeadline from '../../../components/FilterHeadline';
import FilterInputText from '../../../components/FilterInputText';
import MesosStateStore from '../../../stores/MesosStateStore';
import Overlay from '../../../structs/Overlay';
import RequestErrorMsg from '../../../components/RequestErrorMsg';
import TaskUtil from '../../../utils/TaskUtil';
import VirtualNetworkUtil from '../../../utils/VirtualNetworkUtil';
import Util from '../../../utils/Util';

const headerMapping = {
  id: 'TASK',
  ip_address: 'AGENT IP',
  port_mappings: 'PORT MAPPINGS'
};
const METHODS_TO_BIND = [
  'handleSearchStringChange',
  'renderID',
  'renderPorts',
  'resetFilter'
];

class VirtualNetworkTaskTab extends mixin(StoreMixin) {
  constructor() {
    super(...arguments);

    this.state = {
      errorMessage: null,
      tasksDataReceived: false
    };

    this.store_listeners = [{
      name: 'state',
      events: ['success', 'error']
    }];

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

  onStateStoreError(errorMessage) {
    this.setState({tasksDataReceived: true, errorMessage});
  }

  onStateStoreSuccess() {
    this.setState({tasksDataReceived: true, errorMessage: null});
  }

  handleSearchStringChange(searchString) {
    this.setState({searchString});
  }

  resetFilter() {
    this.setState({searchString: ''});
  }

  isLoading() {
    return !this.state.tasksDataReceived;
  }

  getFilteredTasks(tasks, searchString = '') {
    if (searchString === '') {
      return tasks;
    }

    return tasks.filter(function (task) {
      return task.name.includes(searchString) ||
        task.id.includes(searchString);
    });
  }

  getClassName(prop, sortBy) {
    return classNames({
      'highlight': prop === sortBy.prop
    });
  }

  getColumns() {
    let getClassName = this.getClassName;
    let heading = this.renderHeading;

    return [
      {
        className: getClassName,
        headerClassName: getClassName,
        heading,
        prop: 'id',
        render: this.renderID,
        sortable: false
      },
      {
        className: getClassName,
        getValue: function (task) {
          return Util.findNestedPropertyInObject(
            task,
            'statuses.0.container_status.network_infos.0.ip_address'
          );
        },
        headerClassName: getClassName,
        heading,
        prop: 'ip_address',
        sortable: false
      },
      {
        className: getClassName,
        getValue: TaskUtil.getPortMappings,
        render: this.renderPorts,
        headerClassName: getClassName,
        heading,
        prop: 'port_mappings',
        sortable: false
      }
    ];
  }

  getColGroup() {
    return (
      <colgroup>
        <col style={{width: '50%'}} />
        <col />
        <col />
      </colgroup>
    );
  }

  getErrorScreen() {
    return <RequestErrorMsg />;
  }

  getLoadingScreen() {
    return (

    );
  }

  getTaskLink(taskID, value, title) {
    if (!value) {
      value = taskID;
    }

    if (!title) {
      title = taskID;
    }

    return (
      <Link
        className="clickable"
        params={{overlayName: this.props.overlay.getName(), taskID}}
        title={title}
        to="virtual-networks-tab-detail-tasks-detail">
        {value}
      </Link>
    );
  }

  getTitle(portMappings) {
    return portMappings.map(function (mapping) {
      return `${mapping.container_port} > ${mapping.host_port} (${mapping.protocol})`
    }).join(', ')
  }

  renderHeading(prop) {
    return (
      {headerMapping[prop]}
    );
  }

  renderID(prop, task) {
    return this.getTaskLink(task.id);
  }

  renderPorts(prop, task) {
    let portMappings = TaskUtil.getPortMappings(task);
    if (!portMappings) {
      return 'N/A';
    }

    let title = this.getTitle(portMappings);
    if (portMappings.length > 3) {
      portMappings = portMappings.slice(0, 2);
      portMappings.push({container_port: '...'});
    }

    let {id} = task;
    return portMappings.map((mapping, index) => {
      let mapTo = [];

      if (mapping.host_port) {
        mapTo.push(

        );
        mapTo.push(this.getTaskLink(
          id,
          `${mapping.host_port} (${mapping.protocol})`,
          title
        ));
      }

      return (

              {this.getTaskLink(id, mapping.container_port, title)}
              {mapTo}

      );
    });
  }

  render() {
    let {errorMessage, searchString} = this.state;
    if (this.isLoading()) {
      return this.getLoadingScreen();
    }

    if (errorMessage) {
      return this.getErrorScreen();
    }

    let {overlay} = this.props;
    if (!overlay) {
      return VirtualNetworkUtil.getEmptyNetworkScreen();
    }

    let tasks = MesosStateStore.getTasksFromVirtualNetworkName(
      overlay.getName()
    );
    let filteredTasks = this.getFilteredTasks(tasks, searchString);

    return (

        <FilterHeadline
          inverseStyle={true}
          onReset={this.resetFilter}
          name="Task"
          currentLength={filteredTasks.length}
          totalLength={tasks.length} />
        <FilterBar>
          <FilterInputText
            searchString={searchString}
            handleFilterChange={this.handleSearchStringChange}
            inverseStyle={true} />
        </FilterBar>

    );
  }
}

VirtualNetworkTaskTab.contextTypes = {
  router: React.PropTypes.func
};

VirtualNetworkTaskTab.propTypes = {
  overlay: React.PropTypes.instanceOf(Overlay)
}

module.exports = VirtualNetworkTaskTab;
<commit_msg>Show N/A when ip is not available<commit_after>import classNames from 'classnames';
import {Link} from 'react-router';
import mixin from 'reactjs-mixin';
/* eslint-disable no-unused-vars */
import React from 'react';
/* eslint-enable no-unused-vars */
import {StoreMixin} from 'mesosphere-shared-reactjs';
import {Table} from 'reactjs-components';

import FilterBar from '../../../components/FilterBar';
import FilterHeadline from '../../../components/FilterHeadline';
import FilterInputText from '../../../components/FilterInputText';
import MesosStateStore from '../../../stores/MesosStateStore';
import Overlay from '../../../structs/Overlay';
import RequestErrorMsg from '../../../components/RequestErrorMsg';
import TaskUtil from '../../../utils/TaskUtil';
import VirtualNetworkUtil from '../../../utils/VirtualNetworkUtil';
import Util from '../../../utils/Util';

const headerMapping = {
  id: 'TASK',
  ip_address: 'AGENT IP',
  port_mappings: 'PORT MAPPINGS'
};
const METHODS_TO_BIND = [
  'handleSearchStringChange',
  'renderID',
  'renderPorts',
  'resetFilter'
];

class VirtualNetworkTaskTab extends mixin(StoreMixin) {
  constructor() {
    super(...arguments);

    this.state = {
      errorMessage: null,
      tasksDataReceived: false
    };

    this.store_listeners = [{
      name: 'state',
      events: ['success', 'error']
    }];

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

  onStateStoreError(errorMessage) {
    this.setState({tasksDataReceived: true, errorMessage});
  }

  onStateStoreSuccess() {
    this.setState({tasksDataReceived: true, errorMessage: null});
  }

  handleSearchStringChange(searchString) {
    this.setState({searchString});
  }

  resetFilter() {
    this.setState({searchString: ''});
  }

  isLoading() {
    return !this.state.tasksDataReceived;
  }

  getFilteredTasks(tasks, searchString = '') {
    if (searchString === '') {
      return tasks;
    }

    return tasks.filter(function (task) {
      return task.name.includes(searchString) ||
        task.id.includes(searchString);
    });
  }

  getClassName(prop, sortBy) {
    return classNames({
      'highlight': prop === sortBy.prop
    });
  }

  getColumns() {
    let getClassName = this.getClassName;
    let heading = this.renderHeading;

    return [
      {
        className: getClassName,
        headerClassName: getClassName,
        heading,
        prop: 'id',
        render: this.renderID,
        sortable: false
      },
      {
        className: getClassName,
        getValue: function (task) {
          return Util.findNestedPropertyInObject(
            task,
            'statuses.0.container_status.network_infos.0.ip_address'
          );
        },
        headerClassName: getClassName,
        heading,
        prop: 'ip_address',
        render: this.renderAgentIP,
        sortable: false
      },
      {
        className: getClassName,
        getValue: TaskUtil.getPortMappings,
        headerClassName: getClassName,
        heading,
        prop: 'port_mappings',
        render: this.renderPorts,
        sortable: false
      }
    ];
  }

  getColGroup() {
    return (
      <colgroup>
        <col style={{width: '50%'}} />
        <col />
        <col />
      </colgroup>
    );
  }

  getErrorScreen() {
    return <RequestErrorMsg />;
  }

  getLoadingScreen() {
    return (

    );
  }

  getTaskLink(taskID, value, title) {
    if (!value) {
      value = taskID;
    }

    if (!title) {
      title = taskID;
    }

    return (
      <Link
        className="clickable"
        params={{overlayName: this.props.overlay.getName(), taskID}}
        title={title}
        to="virtual-networks-tab-detail-tasks-detail">
        {value}
      </Link>
    );
  }

  getTitle(portMappings) {
    return portMappings.map(function (mapping) {
      return `${mapping.container_port} > ${mapping.host_port} (${mapping.protocol})`
    }).join(', ')
  }

  renderAgentIP(task) {
    let ipAddress = Util.findNestedPropertyInObject(
      task,
      'statuses.0.container_status.network_infos.0.ip_address'
    );

    if (!ipAddress) {
      return 'N/A';
    }

    return ipAddress;
  }

  renderHeading(prop) {
    return (
      {headerMapping[prop]}
    );
  }

  renderID(prop, task) {
    return this.getTaskLink(task.id);
  }

  renderPorts(prop, task) {
    let portMappings = TaskUtil.getPortMappings(task);
    if (!portMappings) {
      return 'N/A';
    }

    let title = this.getTitle(portMappings);
    if (portMappings.length > 3) {
      portMappings = portMappings.slice(0, 2);
      portMappings.push({container_port: '...'});
    }

    let {id} = task;
    return portMappings.map((mapping, index) => {
      let mapTo = [];

      if (mapping.host_port) {
        mapTo.push(

        );
        mapTo.push(this.getTaskLink(
          id,
          `${mapping.host_port} (${mapping.protocol})`,
          title
        ));
      }

      return (

              {this.getTaskLink(id, mapping.container_port, title)}
              {mapTo}

      );
    });
  }

  render() {
    let {errorMessage, searchString} = this.state;
    if (this.isLoading()) {
      return this.getLoadingScreen();
    }

    if (errorMessage) {
      return this.getErrorScreen();
    }

    let {overlay} = this.props;
    if (!overlay) {
      return VirtualNetworkUtil.getEmptyNetworkScreen();
    }

    let tasks = MesosStateStore.getTasksFromVirtualNetworkName(
      overlay.getName()
    );
    let filteredTasks = this.getFilteredTasks(tasks, searchString);

    return (

        <FilterHeadline
          inverseStyle={true}
          onReset={this.resetFilter}
          name="Task"
          currentLength={filteredTasks.length}
          totalLength={tasks.length} />
        <FilterBar>
          <FilterInputText
            searchString={searchString}
            handleFilterChange={this.handleSearchStringChange}
            inverseStyle={true} />
        </FilterBar>

    );
  }
}

VirtualNetworkTaskTab.contextTypes = {
  router: React.PropTypes.func
};

VirtualNetworkTaskTab.propTypes = {
  overlay: React.PropTypes.instanceOf(Overlay)
}

module.exports = VirtualNetworkTaskTab;
<|endoftext|>


<commit_before>const DocumentManager = require('./DocumentManager')
const HistoryManager = require('./HistoryManager')
const ProjectManager = require('./ProjectManager')
const Errors = require('./Errors')
const logger = require('logger-sharelatex')
const Metrics = require('./Metrics')
const ProjectFlusher = require('./ProjectFlusher')
const DeleteQueueManager = require('./DeleteQueueManager')
const async = require('async')

const TWO_MEGABYTES = 2 * 1024 * 1024

module.exports = {
  getDoc,
  getProjectDocsAndFlushIfOld,
  clearProjectState,
  setDoc,
  flushDocIfLoaded,
  deleteDoc,
  flushProject,
  deleteProject,
  deleteMultipleProjects,
  acceptChanges,
  deleteComment,
  updateProject,
  resyncProjectHistory,
  flushAllProjects,
  flushQueuedProjects
}

function getDoc(req, res, next) {
  let fromVersion
  const docId = req.params.doc_id
  const projectId = req.params.project_id
  logger.log({ projectId, docId }, 'getting doc via http')
  const timer = new Metrics.Timer('http.getDoc')

  if (req.query.fromVersion != null) {
    fromVersion = parseInt(req.query.fromVersion, 10)
  } else {
    fromVersion = -1
  }

  DocumentManager.getDocAndRecentOpsWithLock(
    projectId,
    docId,
    fromVersion,
    function (error, lines, version, ops, ranges, pathname) {
      timer.done()
      if (error) {
        return next(error)
      }
      logger.log({ projectId, docId }, 'got doc via http')
      if (lines == null || version == null) {
        return next(new Errors.NotFoundError('document not found'))
      }
      res.json({
        id: docId,
        lines,
        version,
        ops,
        ranges,
        pathname
      })
    }
  )
}

function _getTotalSizeOfLines(lines) {
  let size = 0
  for (const line of lines) {
    size += line.length + 1
  }
  return size
}

function getProjectDocsAndFlushIfOld(req, res, next) {
  const projectId = req.params.project_id
  const projectStateHash = req.query.state
  // exclude is string of existing docs "id:version,id:version,..."
  const excludeItems =
    req.query.exclude != null ? req.query.exclude.split(',') : []
  logger.log({ projectId, exclude: excludeItems }, 'getting docs via http')
  const timer = new Metrics.Timer('http.getAllDocs')
  const excludeVersions = {}
  for (const item of excludeItems) {
    const [id, version] = item.split(':')
    excludeVersions[id] = version
  }
  logger.log(
    { projectId, projectStateHash, excludeVersions },
    'excluding versions'
  )
  ProjectManager.getProjectDocsAndFlushIfOld(
    projectId,
    projectStateHash,
    excludeVersions,
    function (error, result) {
      timer.done()
      if (error instanceof Errors.ProjectStateChangedError) {
        res.sendStatus(409) // conflict
      } else if (error) {
        next(error)
      } else {
        logger.log(
          {
            projectId,
            result: result.map((doc) => `${doc._id}:${doc.v}`)
          },
          'got docs via http'
        )
        res.send(result)
      }
    }
  )
}

function clearProjectState(req, res, next) {
  const projectId = req.params.project_id
  const timer = new Metrics.Timer('http.clearProjectState')
  logger.log({ projectId }, 'clearing project state via http')
  ProjectManager.clearProjectState(projectId, function (error) {
    timer.done()
    if (error) {
      next(error)
    } else {
      res.sendStatus(200)
    }
  })
}

function setDoc(req, res, next) {
  const docId = req.params.doc_id
  const projectId = req.params.project_id
  const { lines, source, user_id: userId, undoing } = req.body
  const lineSize = _getTotalSizeOfLines(lines)
  if (lineSize > TWO_MEGABYTES) {
    logger.log(
      { projectId, docId, source, lineSize, userId },
      'document too large, returning 406 response'
    )
    return res.sendStatus(406)
  }
  logger.log(
    { projectId, docId, lines, source, userId, undoing },
    'setting doc via http'
  )
  const timer = new Metrics.Timer('http.setDoc')
  DocumentManager.setDocWithLock(
    projectId,
    docId,
    lines,
    source,
    userId,
    undoing,
    function (error) {
      timer.done()
      if (error) {
        return next(error)
      }
      logger.log({ projectId, docId }, 'set doc via http')
      res.sendStatus(204) // No Content
    }
  )
}

function flushDocIfLoaded(req, res, next) {
  const docId = req.params.doc_id
  const projectId = req.params.project_id
  logger.log({ projectId, docId }, 'flushing doc via http')
  const timer = new Metrics.Timer('http.flushDoc')
  DocumentManager.flushDocIfLoadedWithLock(projectId, docId, function (error) {
    timer.done()
    if (error) {
      return next(error)
    }
    logger.log({ projectId, docId }, 'flushed doc via http')
    res.sendStatus(204) // No Content
  })
}

function deleteDoc(req, res, next) {
  const docId = req.params.doc_id
  const projectId = req.params.project_id
  const ignoreFlushErrors = req.query.ignore_flush_errors === 'true'
  const timer = new Metrics.Timer('http.deleteDoc')
  logger.log({ projectId, docId }, 'deleting doc via http')
  DocumentManager.flushAndDeleteDocWithLock(
    projectId,
    docId,
    { ignoreFlushErrors },
    function (error) {
      timer.done()
      // There is no harm in flushing project history if the previous call
      // failed and sometimes it is required
      HistoryManager.flushProjectChangesAsync(projectId)

      if (error) {
        return next(error)
      }
      logger.log({ projectId, docId }, 'deleted doc via http')
      res.sendStatus(204) // No Content
    }
  )
}

function flushProject(req, res, next) {
  const projectId = req.params.project_id
  logger.log({ projectId }, 'flushing project via http')
  const timer = new Metrics.Timer('http.flushProject')
  ProjectManager.flushProjectWithLocks(projectId, function (error) {
    timer.done()
    if (error) {
      return next(error)
    }
    logger.log({ projectId }, 'flushed project via http')
    res.sendStatus(204) // No Content
  })
}

function deleteProject(req, res, next) {
  const projectId = req.params.project_id
  logger.log({ projectId }, 'deleting project via http')
  const options = {}
  if (req.query.background) {
    options.background = true
  } // allow non-urgent flushes to be queued
  if (req.query.shutdown) {
    options.skip_history_flush = true
  } // don't flush history when realtime shuts down
  if (req.query.background) {
    ProjectManager.queueFlushAndDeleteProject(projectId, function (error) {
      if (error) {
        return next(error)
      }
      logger.log({ projectId }, 'queue delete of project via http')
      res.sendStatus(204)
    }) // No Content
  } else {
    const timer = new Metrics.Timer('http.deleteProject')
    ProjectManager.flushAndDeleteProjectWithLocks(projectId, options, function (
      error
    ) {
      timer.done()
      if (error) {
        return next(error)
      }
      logger.log({ projectId }, 'deleted project via http')
      res.sendStatus(204) // No Content
    })
  }
}

function deleteMultipleProjects(req, res, next) {
  const projectIds = req.body.project_ids || []
  logger.log({ projectIds }, 'deleting multiple projects via http')
  async.eachSeries(
    projectIds,
    function (projectId, cb) {
      logger.log({ projectId }, 'queue delete of project via http')
      ProjectManager.queueFlushAndDeleteProject(projectId, cb)
    },
    function (error) {
      if (error) {
        return next(error)
      }
      res.sendStatus(204) // No Content
    }
  )
}

function acceptChanges(req, res, next) {
  const { project_id: projectId, doc_id: docId } = req.params
  let changeIds = req.body.change_ids
  if (changeIds == null) {
    changeIds = [req.params.change_id]
  }
  logger.log(
    { projectId, docId },
    `accepting ${changeIds.length} changes via http`
  )
  const timer = new Metrics.Timer('http.acceptChanges')
  DocumentManager.acceptChangesWithLock(projectId, docId, changeIds, function (
    error
  ) {
    timer.done()
    if (error) {
      return next(error)
    }
    logger.log(
      { projectId, docId },
      `accepted ${changeIds.length} changes via http`
    )
    res.sendStatus(204) // No Content
  })
}

function deleteComment(req, res, next) {
  const {
    project_id: projectId,
    doc_id: docId,
    comment_id: commentId
  } = req.params
  logger.log({ projectId, docId, commentId }, 'deleting comment via http')
  const timer = new Metrics.Timer('http.deleteComment')
  DocumentManager.deleteCommentWithLock(projectId, docId, commentId, function (
    error
  ) {
    timer.done()
    if (error) {
      return next(error)
    }
    logger.log({ projectId, docId, commentId }, 'deleted comment via http')
    res.sendStatus(204) // No Content
  })
}

function updateProject(req, res, next) {
  const timer = new Metrics.Timer('http.updateProject')
  const projectId = req.params.project_id
  const {
    projectHistoryId,
    userId,
    docUpdates,
    fileUpdates,
    version
  } = req.body
  logger.log(
    { projectId, docUpdates, fileUpdates, version },
    'updating project via http'
  )

  ProjectManager.updateProjectWithLocks(
    projectId,
    projectHistoryId,
    userId,
    docUpdates,
    fileUpdates,
    version,
    function (error) {
      timer.done()
      if (error) {
        return next(error)
      }
      logger.log({ projectId }, 'updated project via http')
      res.sendStatus(204) // No Content
    }
  )
}

function resyncProjectHistory(req, res, next) {
  const projectId = req.params.project_id
  const { projectHistoryId, docs, files } = req.body

  logger.log(
    { projectId, docs, files },
    'queuing project history resync via http'
  )
  HistoryManager.resyncProjectHistory(
    projectId,
    projectHistoryId,
    docs,
    files,
    function (error) {
      if (error) {
        return next(error)
      }
      logger.log({ projectId }, 'queued project history resync via http')
      res.sendStatus(204)
    }
  )
}

function flushAllProjects(req, res, next) {
  res.setTimeout(5 * 60 * 1000)
  const options = {
    limit: req.query.limit || 1000,
    concurrency: req.query.concurrency || 5,
    dryRun: req.query.dryRun || false
  }
  ProjectFlusher.flushAllProjects(options, function (err, projectIds) {
    if (err) {
      logger.err({ err }, 'error bulk flushing projects')
      res.sendStatus(500)
    } else {
      res.send(projectIds)
    }
  })
}

function flushQueuedProjects(req, res, next) {
  res.setTimeout(10 * 60 * 1000)
  const options = {
    limit: req.query.limit || 1000,
    timeout: 5 * 60 * 1000,
    min_delete_age: req.query.min_delete_age || 5 * 60 * 1000
  }
  DeleteQueueManager.flushAndDeleteOldProjects(options, function (
    err,
    flushed
  ) {
    if (err) {
      logger.err({ err }, 'error flushing old projects')
      res.sendStatus(500)
    } else {
      logger.log({ flushed }, 'flush of queued projects completed')
      res.send({ flushed })
    }
  })
}
<commit_msg>Decaf cleanup: use arrow functions for callbacks<commit_after>const DocumentManager = require('./DocumentManager')
const HistoryManager = require('./HistoryManager')
const ProjectManager = require('./ProjectManager')
const Errors = require('./Errors')
const logger = require('logger-sharelatex')
const Metrics = require('./Metrics')
const ProjectFlusher = require('./ProjectFlusher')
const DeleteQueueManager = require('./DeleteQueueManager')
const async = require('async')

const TWO_MEGABYTES = 2 * 1024 * 1024

module.exports = {
  getDoc,
  getProjectDocsAndFlushIfOld,
  clearProjectState,
  setDoc,
  flushDocIfLoaded,
  deleteDoc,
  flushProject,
  deleteProject,
  deleteMultipleProjects,
  acceptChanges,
  deleteComment,
  updateProject,
  resyncProjectHistory,
  flushAllProjects,
  flushQueuedProjects
}

function getDoc(req, res, next) {
  let fromVersion
  const docId = req.params.doc_id
  const projectId = req.params.project_id
  logger.log({ projectId, docId }, 'getting doc via http')
  const timer = new Metrics.Timer('http.getDoc')

  if (req.query.fromVersion != null) {
    fromVersion = parseInt(req.query.fromVersion, 10)
  } else {
    fromVersion = -1
  }

  DocumentManager.getDocAndRecentOpsWithLock(
    projectId,
    docId,
    fromVersion,
    (error, lines, version, ops, ranges, pathname) => {
      timer.done()
      if (error) {
        return next(error)
      }
      logger.log({ projectId, docId }, 'got doc via http')
      if (lines == null || version == null) {
        return next(new Errors.NotFoundError('document not found'))
      }
      res.json({
        id: docId,
        lines,
        version,
        ops,
        ranges,
        pathname
      })
    }
  )
}

function _getTotalSizeOfLines(lines) {
  let size = 0
  for (const line of lines) {
    size += line.length + 1
  }
  return size
}

function getProjectDocsAndFlushIfOld(req, res, next) {
  const projectId = req.params.project_id
  const projectStateHash = req.query.state
  // exclude is string of existing docs "id:version,id:version,..."
  const excludeItems =
    req.query.exclude != null ? req.query.exclude.split(',') : []
  logger.log({ projectId, exclude: excludeItems }, 'getting docs via http')
  const timer = new Metrics.Timer('http.getAllDocs')
  const excludeVersions = {}
  for (const item of excludeItems) {
    const [id, version] = item.split(':')
    excludeVersions[id] = version
  }
  logger.log(
    { projectId, projectStateHash, excludeVersions },
    'excluding versions'
  )
  ProjectManager.getProjectDocsAndFlushIfOld(
    projectId,
    projectStateHash,
    excludeVersions,
    (error, result) => {
      timer.done()
      if (error instanceof Errors.ProjectStateChangedError) {
        res.sendStatus(409) // conflict
      } else if (error) {
        next(error)
      } else {
        logger.log(
          {
            projectId,
            result: result.map((doc) => `${doc._id}:${doc.v}`)
          },
          'got docs via http'
        )
        res.send(result)
      }
    }
  )
}

function clearProjectState(req, res, next) {
  const projectId = req.params.project_id
  const timer = new Metrics.Timer('http.clearProjectState')
  logger.log({ projectId }, 'clearing project state via http')
  ProjectManager.clearProjectState(projectId, (error) => {
    timer.done()
    if (error) {
      next(error)
    } else {
      res.sendStatus(200)
    }
  })
}

function setDoc(req, res, next) {
  const docId = req.params.doc_id
  const projectId = req.params.project_id
  const { lines, source, user_id: userId, undoing } = req.body
  const lineSize = _getTotalSizeOfLines(lines)
  if (lineSize > TWO_MEGABYTES) {
    logger.log(
      { projectId, docId, source, lineSize, userId },
      'document too large, returning 406 response'
    )
    return res.sendStatus(406)
  }
  logger.log(
    { projectId, docId, lines, source, userId, undoing },
    'setting doc via http'
  )
  const timer = new Metrics.Timer('http.setDoc')
  DocumentManager.setDocWithLock(
    projectId,
    docId,
    lines,
    source,
    userId,
    undoing,
    (error) => {
      timer.done()
      if (error) {
        return next(error)
      }
      logger.log({ projectId, docId }, 'set doc via http')
      res.sendStatus(204) // No Content
    }
  )
}

function flushDocIfLoaded(req, res, next) {
  const docId = req.params.doc_id
  const projectId = req.params.project_id
  logger.log({ projectId, docId }, 'flushing doc via http')
  const timer = new Metrics.Timer('http.flushDoc')
  DocumentManager.flushDocIfLoadedWithLock(projectId, docId, (error) => {
    timer.done()
    if (error) {
      return next(error)
    }
    logger.log({ projectId, docId }, 'flushed doc via http')
    res.sendStatus(204) // No Content
  })
}

function deleteDoc(req, res, next) {
  const docId = req.params.doc_id
  const projectId = req.params.project_id
  const ignoreFlushErrors = req.query.ignore_flush_errors === 'true'
  const timer = new Metrics.Timer('http.deleteDoc')
  logger.log({ projectId, docId }, 'deleting doc via http')
  DocumentManager.flushAndDeleteDocWithLock(
    projectId,
    docId,
    { ignoreFlushErrors },
    (error) => {
      timer.done()
      // There is no harm in flushing project history if the previous call
      // failed and sometimes it is required
      HistoryManager.flushProjectChangesAsync(projectId)

      if (error) {
        return next(error)
      }
      logger.log({ projectId, docId }, 'deleted doc via http')
      res.sendStatus(204) // No Content
    }
  )
}

function flushProject(req, res, next) {
  const projectId = req.params.project_id
  logger.log({ projectId }, 'flushing project via http')
  const timer = new Metrics.Timer('http.flushProject')
  ProjectManager.flushProjectWithLocks(projectId, (error) => {
    timer.done()
    if (error) {
      return next(error)
    }
    logger.log({ projectId }, 'flushed project via http')
    res.sendStatus(204) // No Content
  })
}

function deleteProject(req, res, next) {
  const projectId = req.params.project_id
  logger.log({ projectId }, 'deleting project via http')
  const options = {}
  if (req.query.background) {
    options.background = true
  } // allow non-urgent flushes to be queued
  if (req.query.shutdown) {
    options.skip_history_flush = true
  } // don't flush history when realtime shuts down
  if (req.query.background) {
    ProjectManager.queueFlushAndDeleteProject(projectId, (error) => {
      if (error) {
        return next(error)
      }
      logger.log({ projectId }, 'queue delete of project via http')
      res.sendStatus(204)
    }) // No Content
  } else {
    const timer = new Metrics.Timer('http.deleteProject')
    ProjectManager.flushAndDeleteProjectWithLocks(
      projectId,
      options,
      (error) => {
        timer.done()
        if (error) {
          return next(error)
        }
        logger.log({ projectId }, 'deleted project via http')
        res.sendStatus(204) // No Content
      }
    )
  }
}

function deleteMultipleProjects(req, res, next) {
  const projectIds = req.body.project_ids || []
  logger.log({ projectIds }, 'deleting multiple projects via http')
  async.eachSeries(
    projectIds,
    (projectId, cb) => {
      logger.log({ projectId }, 'queue delete of project via http')
      ProjectManager.queueFlushAndDeleteProject(projectId, cb)
    },
    (error) => {
      if (error) {
        return next(error)
      }
      res.sendStatus(204) // No Content
    }
  )
}

function acceptChanges(req, res, next) {
  const { project_id: projectId, doc_id: docId } = req.params
  let changeIds = req.body.change_ids
  if (changeIds == null) {
    changeIds = [req.params.change_id]
  }
  logger.log(
    { projectId, docId },
    `accepting ${changeIds.length} changes via http`
  )
  const timer = new Metrics.Timer('http.acceptChanges')
  DocumentManager.acceptChangesWithLock(
    projectId,
    docId,
    changeIds,
    (error) => {
      timer.done()
      if (error) {
        return next(error)
      }
      logger.log(
        { projectId, docId },
        `accepted ${changeIds.length} changes via http`
      )
      res.sendStatus(204) // No Content
    }
  )
}

function deleteComment(req, res, next) {
  const {
    project_id: projectId,
    doc_id: docId,
    comment_id: commentId
  } = req.params
  logger.log({ projectId, docId, commentId }, 'deleting comment via http')
  const timer = new Metrics.Timer('http.deleteComment')
  DocumentManager.deleteCommentWithLock(
    projectId,
    docId,
    commentId,
    (error) => {
      timer.done()
      if (error) {
        return next(error)
      }
      logger.log({ projectId, docId, commentId }, 'deleted comment via http')
      res.sendStatus(204) // No Content
    }
  )
}

function updateProject(req, res, next) {
  const timer = new Metrics.Timer('http.updateProject')
  const projectId = req.params.project_id
  const {
    projectHistoryId,
    userId,
    docUpdates,
    fileUpdates,
    version
  } = req.body
  logger.log(
    { projectId, docUpdates, fileUpdates, version },
    'updating project via http'
  )

  ProjectManager.updateProjectWithLocks(
    projectId,
    projectHistoryId,
    userId,
    docUpdates,
    fileUpdates,
    version,
    (error) => {
      timer.done()
      if (error) {
        return next(error)
      }
      logger.log({ projectId }, 'updated project via http')
      res.sendStatus(204) // No Content
    }
  )
}

function resyncProjectHistory(req, res, next) {
  const projectId = req.params.project_id
  const { projectHistoryId, docs, files } = req.body

  logger.log(
    { projectId, docs, files },
    'queuing project history resync via http'
  )
  HistoryManager.resyncProjectHistory(
    projectId,
    projectHistoryId,
    docs,
    files,
    (error) => {
      if (error) {
        return next(error)
      }
      logger.log({ projectId }, 'queued project history resync via http')
      res.sendStatus(204)
    }
  )
}

function flushAllProjects(req, res, next) {
  res.setTimeout(5 * 60 * 1000)
  const options = {
    limit: req.query.limit || 1000,
    concurrency: req.query.concurrency || 5,
    dryRun: req.query.dryRun || false
  }
  ProjectFlusher.flushAllProjects(options, (err, projectIds) => {
    if (err) {
      logger.err({ err }, 'error bulk flushing projects')
      res.sendStatus(500)
    } else {
      res.send(projectIds)
    }
  })
}

function flushQueuedProjects(req, res, next) {
  res.setTimeout(10 * 60 * 1000)
  const options = {
    limit: req.query.limit || 1000,
    timeout: 5 * 60 * 1000,
    min_delete_age: req.query.min_delete_age || 5 * 60 * 1000
  }
  DeleteQueueManager.flushAndDeleteOldProjects(options, (err, flushed) => {
    if (err) {
      logger.err({ err }, 'error flushing old projects')
      res.sendStatus(500)
    } else {
      logger.log({ flushed }, 'flush of queued projects completed')
      res.send({ flushed })
    }
  })
}
<|endoftext|>


<commit_before>//
// Strider Worker extension for Sauce Labs tests
//

var check = require('httpcheck')
var fs = require('fs')
var path = require('path')
var request = require('request')
var wd = require('wd')

// Port on which the application-under-test webserver should bind to on localhost.
// Sauce Connector will tunnel from this to Sauce Cloud for Selenium tests
var HTTP_PORT
//
// Path to SauceLabs Connector PID file
var PIDFILE = path.join(__dirname, "tunnel.pid")

// SauceLabs test timeout in ms
// Permits self-healing in worst-case hang
var SAUCE_TEST_TIMEOUT = 1000 * 60 * 45 // 45 minutes

var connectorProc

var cleanupRun = false

// Read & parse a JSON file
function getJson(filename, cb) {
  fs.readFile(filename, function(err, data) {
    if (err) return cb(err, null)
    try {
      var json = JSON.parse(data)
      cb(null, json)
    } catch(e) {
      cb(e, null)
    }
  })
}

// Is Sauce configured for this context?
function sauceConfigured(ctx) {
  var sauceAccessKey = ctx.jobData.repo_config.sauce_access_key
  var sauceUsername = ctx.jobData.repo_config.sauce_username

  if (sauceAccessKey === undefined
    || sauceUsername === undefined) {
    return false
  }

  return true
}

// This will shut down the tunnel in a nice way
function cleanup(ctx, cb) {
  if (!sauceConfigured(ctx)) {
    return cb(0)
  }
  cleanupRun = true
  var msg = "Shutting down Sauce Connector"
  console.log(msg)
  ctx.striderMessage(msg)
  if (connectorProc) connectorProc.kill("SIGINT")
  // Give Sauce Connector 5 seconds to gracefully stop before sending SIGKILL
  setTimeout(function() {
    if (connectorProc) connectorProc.kill("SIGKILL")
    fs.unlink(PIDFILE, function() {})
    msg = "Sauce Connector successfully shut down"
    console.log(msg)
    ctx.striderMessage(msg)

    return cb(0)
  }, 5000)
}

function test(ctx, cb) {

  if (!sauceConfigured(ctx)) {
    return cb()
  }

  var sauceAccessKey = ctx.jobData.repo_config.sauce_access_key
  var sauceUsername = ctx.jobData.repo_config.sauce_username
  var sauceBrowsers = ctx.jobData.repo_config.sauce_browsers

  if (sauceBrowsers === undefined || sauceBrowsers.length === 0) {
    // Default to latest chrome on Windows Vista
    sauceBrowsers = [
      {
        browserName:"chrome",
        version:"",
        platform:"Windows 2008"
      }
    ]
  }

  function log(msg) {
    ctx.striderMessage(msg)
    console.log(msg)
  }

  HTTP_PORT = ctx.browsertestPort || 8031

  ctx.striderMessage("Waiting for webserver to come up on localhost:" + HTTP_PORT)

  check({url:"http://localhost:"+HTTP_PORT+"/", log:log}, function(err) {
    if (err) {
      clearInterval(intervalId)
      return cb(1)
    }
    serverUp()
  })

  // Start the Sauce Connector. Returns childProcess object.
  function startConnector(username, apiKey, exitCb) {
    var jarPath = path.join(__dirname, "thirdparty", "Sauce-Connect.jar")
    var jsh = ctx.shellWrap("exec java -Xmx64m -jar " + jarPath + " " + username + " " + apiKey)

    var done = false
    ctx.striderMessage("Starting Sauce Connector")
    connectorProc = ctx.forkProc(ctx.workingDir, jsh.cmd, jsh.args, exitCb)
    // Wait until connector outputs "You may start your tests"
    // before executing Sauce tests
    connectorProc.stdout.on('data', function(data) {
      if (/Connected! You may start your tests./.exec(data) !== null) {
          var resultsReceived = 0
          var buildStatus = 0
          var resultMessages = []
          var finished = false
          sauceBrowsers.forEach(function(browser) {
            var worker = wd.remote("ondemand.saucelabs.com", 80,
              sauceUsername, sauceAccessKey)
            var browserId = browser.browserName + "-" + browser.version + "-" + browser.platform.replace(" ", "-")
            // ctx.browsertestPort and ctx.browsertestPath come from the `prepare` phase test
            // plugin - e.g. strider-qunit.
            var testUrl = "http://localhost:" +
                ctx.browsertestPort + "/" + browserId + ctx.browsertestPath
            worker.done = false
            worker.init({
              browserName: browser.browserName,
              version: browser.version,
              platform: browser.platform,
              name: ctx.jobData.repo_config.display_url,
              'idle-timeout': SAUCE_TEST_TIMEOUT / 1000
            }, function(err) {
              if (err) {
                log("Error creating Sauce worker: " + err)
                return cb(1)
              }
              worker.get(testUrl, function() {
                log("Created Sauce worker: " + browserId)
              })

            })

            setTimeout(function() {
              if (!worker.done) {
                log("ERROR: Timeout of " + SAUCE_TEST_TIMEOUT + " ms exceeded for " + browserId + " - terminating ")
                ctx.events.emit('testDone', { id: browserId, total:0, failed:1, passed:0, runtime: SAUCE_TEST_TIMEOUT })
              }
            }, SAUCE_TEST_TIMEOUT)
            ctx.events.on('testDone', function(result) {
              if (finished) return
              if (result.id === browserId && worker && !worker.done) {
                resultMessages.push("Results for tests on " + result.id + ": " + result.total + " total " +
                  result.failed + " failed " + result.passed + " passed " + result.runtime + " ms runtime")
                if (result.failed !== 0) {
                  buildStatus = 1
                }
                log("Terminating Sauce worker: " + browserId)
                worker.quit()
                worker.done = true
                resultsReceived++
              }
              // If all the results are in, finish the build
              if (resultsReceived == sauceBrowsers.length) {
                finished = true
                resultMessages.forEach(function(msg) {
                  log(msg)
                })
                cb(buildStatus)
              }
            })
          })
      }
    })
  }
  // Server is up, start Sauce Connector
  function serverUp() {
    console.log("Starting sauce connector")
    startConnector(sauceUsername, sauceAccessKey,
      function(exitCode) {
      console.log("Sauce Connector exited with code: %d", exitCode)
      // If the connector exited before the cleanup phase has run, it failed to start
      if (!cleanupRun) {
        log("Error starting Sauce Connector - failing test")
        cleanupRun = true
        fs.unlink(PIDFILE, function() {})
        return cb(1)
      }
    })
  }
}

module.exports = function(ctx, cb) {

  ctx.addBuildHook({
    cleanup:cleanup,
    test:test
  })

  console.log("strider-sauce worker extension loaded")
  cb(null, null)
}
<commit_msg>zap stray line<commit_after>//
// Strider Worker extension for Sauce Labs tests
//

var check = require('httpcheck')
var fs = require('fs')
var path = require('path')
var request = require('request')
var wd = require('wd')

// Port on which the application-under-test webserver should bind to on localhost.
// Sauce Connector will tunnel from this to Sauce Cloud for Selenium tests
var HTTP_PORT
//
// Path to SauceLabs Connector PID file
var PIDFILE = path.join(__dirname, "tunnel.pid")

// SauceLabs test timeout in ms
// Permits self-healing in worst-case hang
var SAUCE_TEST_TIMEOUT = 1000 * 60 * 45 // 45 minutes

var connectorProc

var cleanupRun = false

// Read & parse a JSON file
function getJson(filename, cb) {
  fs.readFile(filename, function(err, data) {
    if (err) return cb(err, null)
    try {
      var json = JSON.parse(data)
      cb(null, json)
    } catch(e) {
      cb(e, null)
    }
  })
}

// Is Sauce configured for this context?
function sauceConfigured(ctx) {
  var sauceAccessKey = ctx.jobData.repo_config.sauce_access_key
  var sauceUsername = ctx.jobData.repo_config.sauce_username

  if (sauceAccessKey === undefined
    || sauceUsername === undefined) {
    return false
  }

  return true
}

// This will shut down the tunnel in a nice way
function cleanup(ctx, cb) {
  if (!sauceConfigured(ctx)) {
    return cb(0)
  }
  cleanupRun = true
  var msg = "Shutting down Sauce Connector"
  console.log(msg)
  ctx.striderMessage(msg)
  if (connectorProc) connectorProc.kill("SIGINT")
  // Give Sauce Connector 5 seconds to gracefully stop before sending SIGKILL
  setTimeout(function() {
    if (connectorProc) connectorProc.kill("SIGKILL")
    fs.unlink(PIDFILE, function() {})
    msg = "Sauce Connector successfully shut down"
    console.log(msg)
    ctx.striderMessage(msg)

    return cb(0)
  }, 5000)
}

function test(ctx, cb) {

  if (!sauceConfigured(ctx)) {
    return cb()
  }

  var sauceAccessKey = ctx.jobData.repo_config.sauce_access_key
  var sauceUsername = ctx.jobData.repo_config.sauce_username
  var sauceBrowsers = ctx.jobData.repo_config.sauce_browsers

  if (sauceBrowsers === undefined || sauceBrowsers.length === 0) {
    // Default to latest chrome on Windows Vista
    sauceBrowsers = [
      {
        browserName:"chrome",
        version:"",
        platform:"Windows 2008"
      }
    ]
  }

  function log(msg) {
    ctx.striderMessage(msg)
    console.log(msg)
  }

  HTTP_PORT = ctx.browsertestPort || 8031

  ctx.striderMessage("Waiting for webserver to come up on localhost:" + HTTP_PORT)

  check({url:"http://localhost:"+HTTP_PORT+"/", log:log}, function(err) {
    if (err) {
      clearInterval(intervalId)
      return cb(1)
    }
    serverUp()
  })

  // Start the Sauce Connector. Returns childProcess object.
  function startConnector(username, apiKey, exitCb) {
    var jarPath = path.join(__dirname, "thirdparty", "Sauce-Connect.jar")
    var jsh = ctx.shellWrap("exec java -Xmx64m -jar " + jarPath + " " + username + " " + apiKey)

    ctx.striderMessage("Starting Sauce Connector")
    connectorProc = ctx.forkProc(ctx.workingDir, jsh.cmd, jsh.args, exitCb)
    // Wait until connector outputs "You may start your tests"
    // before executing Sauce tests
    connectorProc.stdout.on('data', function(data) {
      if (/Connected! You may start your tests./.exec(data) !== null) {
          var resultsReceived = 0
          var buildStatus = 0
          var resultMessages = []
          var finished = false
          sauceBrowsers.forEach(function(browser) {
            var worker = wd.remote("ondemand.saucelabs.com", 80,
              sauceUsername, sauceAccessKey)
            var browserId = browser.browserName + "-" + browser.version + "-" + browser.platform.replace(" ", "-")
            // ctx.browsertestPort and ctx.browsertestPath come from the `prepare` phase test
            // plugin - e.g. strider-qunit.
            var testUrl = "http://localhost:" +
                ctx.browsertestPort + "/" + browserId + ctx.browsertestPath
            worker.done = false
            worker.init({
              browserName: browser.browserName,
              version: browser.version,
              platform: browser.platform,
              name: ctx.jobData.repo_config.display_url,
              'idle-timeout': SAUCE_TEST_TIMEOUT / 1000
            }, function(err) {
              if (err) {
                log("Error creating Sauce worker: " + err)
                return cb(1)
              }
              worker.get(testUrl, function() {
                log("Created Sauce worker: " + browserId)
              })

            })

            setTimeout(function() {
              if (!worker.done) {
                log("ERROR: Timeout of " + SAUCE_TEST_TIMEOUT + " ms exceeded for " + browserId + " - terminating ")
                ctx.events.emit('testDone', { id: browserId, total:0, failed:1, passed:0, runtime: SAUCE_TEST_TIMEOUT })
              }
            }, SAUCE_TEST_TIMEOUT)
            ctx.events.on('testDone', function(result) {
              if (finished) return
              if (result.id === browserId && worker && !worker.done) {
                resultMessages.push("Results for tests on " + result.id + ": " + result.total + " total " +
                  result.failed + " failed " + result.passed + " passed " + result.runtime + " ms runtime")
                if (result.failed !== 0) {
                  buildStatus = 1
                }
                log("Terminating Sauce worker: " + browserId)
                worker.quit()
                worker.done = true
                resultsReceived++
              }
              // If all the results are in, finish the build
              if (resultsReceived == sauceBrowsers.length) {
                finished = true
                resultMessages.forEach(function(msg) {
                  log(msg)
                })
                cb(buildStatus)
              }
            })
          })
      }
    })
  }
  // Server is up, start Sauce Connector
  function serverUp() {
    console.log("Starting sauce connector")
    startConnector(sauceUsername, sauceAccessKey,
      function(exitCode) {
      console.log("Sauce Connector exited with code: %d", exitCode)
      // If the connector exited before the cleanup phase has run, it failed to start
      if (!cleanupRun) {
        log("Error starting Sauce Connector - failing test")
        cleanupRun = true
        fs.unlink(PIDFILE, function() {})
        return cb(1)
      }
    })
  }
}

module.exports = function(ctx, cb) {

  ctx.addBuildHook({
    cleanup:cleanup,
    test:test
  })

  console.log("strider-sauce worker extension loaded")
  cb(null, null)
}
<|endoftext|>


<commit_before>var Rank = React.createClass({
	render: function() {
		return (

				{this.props.rank}

		)
	}
});

var ArticleLink = React.createClass({
	render: function() {
		return (
			{this.props.title}
		)
	}
});

var OrginalLink = React.createClass({
	render: function() {
		return (

		)
	}
});

var DomainLink = React.createClass({
	render: function() {
		return (

				({this.props.domain})

		)
	}
});

var EntryTitle = React.createClass({
	render: function() {
		return (

				<ArticleLink aid={this.props.aid} title={this.props.title} />
				<OrginalLink url={this.props.url} />
				<DomainLink domain={this.props.domain} />

		)
	}
});

var SpiderTag = React.createClass({
	render: function() {
		var spid = this.props.spider.spid;
		var spname = this.props.spider.spname;
		return (

				[{spname}]

		)
	}
});

var ArticleTag = React.createClass({
	render: function() {
		return (

				{this.props.tag}

	  )
	}
});

var EntryTags = React.createClass({
	render: function() {
		return (

				<SpiderTag spider={this.props.spider} />

		)
	}
});

var Entry = React.createClass({
	render: function() {
		var index = this.props.index;
		var entry = this.props.entry;
		var spider = {spid: entry[5], spname: entry[3]};
		console.log(entry);
		return (

				<Rank rank={index} />
				<EntryTitle aid={entry[0]} title={entry[1]} url={entry[7]} domain={entry[6]} />
				<EntryTags spider={spider} />

		)
	}
});

var Entries = React.createClass({
	render: function() {
		var entries = this.props.entries;
		return (

				{entries.map(function(entry, index) {
					return <Entry index={index} entry={entry} />
				})}

		)
	}
});

var Category = React.createClass({
	onClick: function(category) {
		this.props.onCategoryClick(category);
	},

	render: function() {
		return (

				{this.props.category}

		)
	}
});

var CategoryDiv = React.createClass({
	render: function() {
		var categories = this.props.categories;
		var onCategoryClick = this.props.onCategoryClick;
		return (

					{categories.map(function(category) {
						return <Category category={category} onCategoryClick={onCategoryClick} />;
					})}

		)
	}
});

var ContentDiv = React.createClass({
	loadFromServer: function() {
		console.log("loadFromServer");
		$.getJSON("/api/day", {day: "2016-06-24"}).done(function(data) {
			var categories = [];
			var entries = data["entries"];
			$.each(entries, function(key, val) {
				categories.push(key);
			});
			this.setState({category: categories[0], data: entries});
		}.bind(this));
	},

	getInitialState: function() {
		return {category: "init", data: {init: []}};
	},

	componentDidMount: function() {
		this.loadFromServer();
	},

	onCategoryClick: function(category) {
		console.log(category);
		this.setState({category: category});
	},

	render: function() {
		var categories = [];
		$.each(this.state.data, function(key, val) {
			categories.push(key);
		});

		var entries = this.state.data[this.state.category];

		return (

				<CategoryDiv categories={categories} onCategoryClick={this.onCategoryClick} />

				<Entries entries={entries} />

		)
	}
});

ReactDOM.render(
  <ContentDiv />,
  document.getElementById('content')
);
<commit_msg>react<commit_after>var Rank = React.createClass({
	render: function() {
		return (

				{this.props.rank}

		)
	}
});

var ArticleLink = React.createClass({
	render: function() {
		return (
			{this.props.title}
		)
	}
});

var OrginalLink = React.createClass({
	render: function() {
		return (

		)
	}
});

var DomainLink = React.createClass({
	render: function() {
		return (

				({this.props.domain})

		)
	}
});

var EntryTitle = React.createClass({
	render: function() {
		return (

				<ArticleLink aid={this.props.aid} title={this.props.title} />
				<OrginalLink url={this.props.url} />
				<DomainLink domain={this.props.domain} />

		)
	}
});

var SpiderTag = React.createClass({
	render: function() {
		var spid = this.props.spider.spid;
		var spname = this.props.spider.spname;
		return (

				[{spname}]

		)
	}
});

var ArticleTag = React.createClass({
	render: function() {
		return (

				{this.props.tag}

	  )
	}
});

var EntryTags = React.createClass({
	render: function() {
		var spider = this.props.spider;
		var tags = this.props.tags;
		return (

				<SpiderTag spider={spider} />
				{tags.map(function(tag) { return <ArticleTag tag={tag} />; })}

		)
	}
});

var Entry = React.createClass({
	render: function() {
		var index = this.props.index;
		var entry = this.props.entry;
		var spider = {spid: entry[5], spname: entry[3]};
		var tags = entry[4];
		if (!tags) {
			tags = [];
		}
		console.log(tags);

		return (

				<Rank rank={index} />
				<EntryTitle aid={entry[0]} title={entry[1]} url={entry[7]} domain={entry[6]} />
				<EntryTags spider={spider} tags={tags} />

		)
	}
});

var Entries = React.createClass({
	render: function() {
		var entries = this.props.entries;
		return (

				{entries.map(function(entry, index) {
					return <Entry index={index} entry={entry} />
				})}

		)
	}
});

var Category = React.createClass({
	onClick: function(category) {
		this.props.onCategoryClick(category);
	},

	render: function() {
		return (

				{this.props.category}

		)
	}
});

var CategoryDiv = React.createClass({
	render: function() {
		var categories = this.props.categories;
		var onCategoryClick = this.props.onCategoryClick;
		return (

					{categories.map(function(category) {
						return <Category category={category} onCategoryClick={onCategoryClick} />;
					})}

		)
	}
});

var ContentDiv = React.createClass({
	loadFromServer: function() {
		console.log("loadFromServer");
		$.getJSON("/api/day", {day: "2016-06-24"}).done(function(data) {
			var categories = [];
			var entries = data["entries"];
			$.each(entries, function(key, val) {
				categories.push(key);
			});
			this.setState({category: categories[0], data: entries});
		}.bind(this));
	},

	getInitialState: function() {
		return {category: "init", data: {init: []}};
	},

	componentDidMount: function() {
		this.loadFromServer();
	},

	onCategoryClick: function(category) {
		console.log(category);
		this.setState({category: category});
	},

	render: function() {
		var categories = [];
		$.each(this.state.data, function(key, val) {
			categories.push(key);
		});

		var entries = this.state.data[this.state.category];

		return (

				<CategoryDiv categories={categories} onCategoryClick={this.onCategoryClick} />

				<Entries entries={entries} />

		)
	}
});

ReactDOM.render(
  <ContentDiv />,
  document.getElementById('content')
);
<|endoftext|>