<commit_before>import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import classNames from 'classnames'
import { debounce } from 'lodash'
import Avatar from '../assets/Avatar'
import Block from './Block'
import EmbedBlock from './EmbedBlock'
import ImageBlock from './ImageBlock'
import QuickEmoji from './QuickEmoji'
import RepostBlock from './RepostBlock'
import TextBlock from './TextBlock'
import PostActionBar from './PostActionBar'
import * as ACTION_TYPES from '../../constants/action_types'
import { addDragObject, removeDragObject } from './DragComponent'
import { addInputObject, removeInputObject } from './InputComponent'
import { userRegex } from '../completers/Completer'

class BlockCollection extends Component {

  static propTypes = {
    avatar: PropTypes.object.isRequired,
    blocks: PropTypes.array,
    cancelAction: PropTypes.func.isRequired,
    completions: PropTypes.shape({
      data: PropTypes.array,
      type: PropTypes.string,
    }),
    dispatch: PropTypes.func.isRequired,
    editorId: PropTypes.string.isRequired,
    editorStore: PropTypes.object,
    emoji: PropTypes.object.isRequired,
    isComment: PropTypes.bool,
    repostContent: PropTypes.array,
    shouldLoadFromState: PropTypes.bool,
    shouldPersist: PropTypes.bool,
    submitAction: PropTypes.func.isRequired,
    submitText: PropTypes.string,
  };

  static defaultProps = {
    blocks: [],
    editorStore: {},
    isComment: false,
    repostContent: [],
    shouldLoadFromState: false,
    shouldPersist: false,
    submitText: 'Post',
  };

  componentWillMount() {
    const { blocks, editorStore, repostContent, shouldLoadFromState } = this.props
    this.state = {
      collection: {},
      hideTextTools: true,
      order: [],
    }
    this.uid = 0

    if (repostContent.length) {
      this.add({ kind: 'repost', data: repostContent })
    }
    if (blocks.length) {
      for (const block of blocks) {
        this.add(block, false)
      }
    } else if (shouldLoadFromState && editorStore.editorState) {
      this.state = { ...editorStore.editorState, hideTextTools: true }
      this.uid = Math.max(...editorStore.editorState.order) + 1
    }
    this.persistBlocks = debounce(this.persistBlocks, 300)
  }

  componentDidMount() {
    const { editorId } = this.props
    // TODO: figure out why this is causing a setState warning
    // you can get the error to happen by going to a profile page
    // visiting a post detail and then hitting back, the setState
    // happens in add, but is triggered from this method since
    // commenting it out gets rid of the issue
    this.addEmptyTextBlock()
    addDragObject({ component: this, dragId: editorId })
    addInputObject(this)
  }

  componentWillReceiveProps(nextProps) {
    const { dispatch, editorId, editorStore } = nextProps
    const { collection } = this.state
    let newBlock = null
    switch (editorStore.type) {
      case ACTION_TYPES.EDITOR.APPEND_TEXT:
        if (editorStore.appendText && editorStore.appendText.length) {
          this.appendText(editorStore.appendText)
          dispatch({ type: ACTION_TYPES.EDITOR.CLEAR_APPENDED_TEXT, payload: { editorId } })
        }
        break
      case ACTION_TYPES.POST.TMP_IMAGE_CREATED:
        this.removeEmptyTextBlock()
        newBlock = this.add({ kind: 'image', data: { url: editorStore.url } })
        dispatch({
          type: ACTION_TYPES.POST.IMAGE_BLOCK_CREATED,
          payload: {
            editorId,
            uid: newBlock.uid,
          },
        })
        break
      case ACTION_TYPES.POST.SAVE_IMAGE_SUCCESS:
        newBlock = this.getBlockFromUid(editorStore.uid)
        if (newBlock) {
          collection[this.getBlockIdentifier(editorStore.uid)] = {
            kind: 'image',
            data: {
              url: editorStore.url,
            },
            uid: editorStore.uid,
          }
          this.setState({ collection })
          this.persistBlocks()
        }
        break
      case ACTION_TYPES.POST.POST_PREVIEW_SUCCESS:
        this.removeEmptyTextBlock()
        this.add({ ...editorStore.postPreviews.body[0] })
        break
      default:
        break
    }
  }

  componentDidUpdate(prevProps) {
    const { completions } = this.props
    const prevCompletions = prevProps.completions
    if (prevCompletions && !completions) {
      requestAnimationFrame(() => {
        this.updateTextCollectionData()
      })
    }
  }

  componentWillUnmount() {
    // this.onHideCompleter()
    removeDragObject(this)
    removeInputObject(this)
  }

  onDragStart(props) {
    const { collection } = this.state
    this.blockNode = props.target.parentNode.parentNode
    this.startOffset = this.blockNode.offsetTop
    this.startHeight = this.blockNode.offsetHeight
    this.prevBlock = this.blockNode.previousSibling
    this.nextBlock = this.blockNode.nextSibling
    const dragUid = this.blockNode.dataset.collectionId
    this.dragBlock = collection[this.getBlockIdentifier(dragUid)]
    // swap the dragging block for a
    // normal block and set the height/width
    collection[this.getBlockIdentifier(dragUid)] = {
      data: {
        width: this.blockNode.offsetWidth,
        height: this.blockNode.offsetHeight,
      },
      kind: 'block',
      uid: this.dragBlock.uid,
    }
    this.onDragMove(props)
    this.setState({ collection })
  }

  onDragMove(props) {
    // move the block we are currently dragging
    this.setState({ dragBlockTop: props.dragY + this.startOffset })
    // determine if we should change order
    if (props.dragY < props.lastDragY) {
      this.onDragUp(props)
    } else if (props.dragY > props.lastDragY) {
      this.onDragDown(props)
    }
  }

  onDragUp(props) {
    if (this.prevBlock &&
        !this.prevBlock.classList.contains('readonly') &&
        (props.dragY + this.startOffset) <
        (this.prevBlock.offsetTop + this.prevBlock.offsetHeight * 0.5)) {
      this.onMoveBlock(-1)
    }
  }

  onDragDown(props) {
    if (this.nextBlock &&
        (props.dragY + this.startOffset + this.startHeight) >
        (this.nextBlock.offsetTop + this.nextBlock.offsetHeight * 0.5)) {
      this.onMoveBlock(1)
    }
  }

  onMoveBlock(delta) {
    if (!this.refs.blockPlaceholder) return
    const { order } = this.state
    const dragUid = this.dragBlock.uid
    const index = order.indexOf(dragUid)
    // remove from old spot
    order.splice(index, 1)
    // add to new spot
    order.splice(index + delta, 0, dragUid)
    this.setState({ order })
    const placeholder = this.refs.blockPlaceholder.refs.editorBlock
    // update prev/next blocks
    this.prevBlock = placeholder.previousSibling
    this.nextBlock = placeholder.nextSibling
  }

  onDragEnd() {
    const { collection } = this.state
    // swap the normal block out for
    // the one that was removed initially
    const dragUid = this.dragBlock.uid
    collection[this.getBlockIdentifier(dragUid)] = this.dragBlock
    // order matters here so the dragBlock gets removed
    this.dragBlock = null
    this.setState({ collection, dragBlockTop: null })
    this.addEmptyTextBlock(true)
  }

  onSubmitPost() {
    const { editorId } = this.props
    if (document.activeElement.parentNode.dataset.editorId === editorId) {
      this.submit()
    }
  }

  onInsertEmoji = ({ value }) => {
    this.appendText(value)
  };

  getBlockIdentifier(uid) {
    const { editorId } = this.props
    return `${editorId}_${uid}`
  }

  getBlockFromUid(uid) {
    const { collection } = this.state
    return collection[this.getBlockIdentifier(uid)]
  }

  getBlockElement(block) {
    const { editorId } = this.props
    const blockProps = {
      data: block.data,
      editorId,
      key: block.uid,
      kind: block.kind,
      onRemoveBlock: this.remove,
      ref: `block${block.uid}`,
      uid: block.uid,
    }
    switch (block.kind) {
      case 'block':
        return (
          <Block { ...blockProps } className="BlockPlaceholder" ref="blockPlaceholder" />
        )
      case 'embed':
        return (
          <EmbedBlock { ...blockProps } />
        )
      case 'image':
        return (
          <ImageBlock { ...blockProps } />
        )
      case 'repost':
        return (
          <RepostBlock { ...blockProps } onRemoveBlock={ null } />
        )
      case 'text':
        return (
          <TextBlock
            { ...blockProps }
            onInput={ this.handleTextBlockInput }
          />
        )
      default:
        return null
    }
  }

  // TODO: probably should have the completer
  // dispatch an action that updates the gui
  // and then we can listen to that instead
  // of brute forcing it with dom lookups
  updateTextCollectionData() {
    const { editorId } = this.props
    const { collection, order } = this.state
    for (const uid of order) {
      const block = this.getBlockFromUid(uid)
      if (block && block.kind === 'text') {
        const selector = `[data-editor-id="${editorId}"][data-collection-id="${uid}"]`
        block.data = document.querySelector(selector).textContent
      }
    }
    this.setState({ collection })
  }

  add(block, shouldCheckForEmpty = true) {
    const newBlock = { ...block, uid: this.uid }
    const { collection, order } = this.state
    collection[this.getBlockIdentifier(this.uid)] = newBlock
    order.push(this.uid)
    this.uid++
    // order matters here
    this.setState({ collection, order })
    if (shouldCheckForEmpty) {
      this.addEmptyTextBlock()
    }
    this.persistBlocks()
    return newBlock
  }

  addEmptyTextBlock(shouldCheckForEmpty = false) {
    const { order } = this.state
    requestAnimationFrame(() => {
      if (order.length > 1) {
        const last = this.getBlockFromUid(order[order.length - 1])
        const secondToLast = this.getBlockFromUid(order[order.length - 2])
        if (secondToLast.kind === 'text' &&
            last.kind === 'text' && !last.data.length) {
          return this.remove(last.uid, shouldCheckForEmpty)
        }
      }
      if (!order.length ||
          this.getBlockFromUid(order[order.length - 1]).kind !== 'text') {
        this.add({ kind: 'text', data: '' }, false)
      }
    })
  }

  appendText = (content) => {
    const { collection, order } = this.state
    const textBlocks = order.filter((uid) => this.getBlockFromUid(uid).kind === 'text')
    const lastBlock = this.getBlockFromUid(textBlocks[textBlocks.length - 1])
    if (lastBlock) {
      lastBlock.data += content
      this.setState({ collection })
    }
  };

  remove = (uid, shouldCheckForEmpty = true) => {
    const { collection, order } = this.state
    delete collection[this.getBlockIdentifier(uid)]
    order.splice(order.indexOf(uid), 1)
    // order matters here
    this.setState({ collection, order })
    if (shouldCheckForEmpty) {
      this.addEmptyTextBlock()
    }
    this.persistBlocks()
  };

  persistBlocks() {
    const { dispatch, editorId, shouldPersist } = this.props
    if (!shouldPersist) { return }
    const { collection, order } = this.state
    dispatch({
      type: ACTION_TYPES.POST.PERSIST,
      payload: {
        collection,
        editorId,
        order,
      },
    })
  }

  removeEmptyTextBlock() {
    const { order } = this.state
    if (order.length > 0) {
      const last = this.getBlockFromUid(order[order.length - 1])
      if (last && last.kind === 'text' && !last.data.length) {
        this.remove(last.uid, false)
      }
    }
  }

  handleTextBlockInput = (vo) => {
    const { collection } = this.state
    collection[this.getBlockIdentifier(vo.uid)] = vo
    this.setState({ collection })
    this.persistBlocks()
  };

  submit = () => {
    const { isComment, submitAction } = this.props
    const data = this.serialize()
    submitAction(data)
    if (isComment) {
      this.clearBlocks()
    }
  };

  clearBlocks() {
    this.setState({ collection: {}, order: [] })
    requestAnimationFrame(() => {
      this.uid = 0
      this.addEmptyTextBlock()
    })
  }

  serialize() {
    const { order } = this.state
    const results = []
    for (const uid of order) {
      const block = this.getBlockFromUid(uid)
      switch (block.kind) {
        case 'text':
          if (block.data.length) {
            results.push({ kind: block.kind, data: block.data })
          }
          break
        case 'repost':
          break
        default:
          results.push({ kind: block.kind, data: block.data })
          break
      }
    }
    return results
  }

  hasMention() {
    const { order } = this.state
    for (const uid of order) {
      const block = this.getBlockFromUid(uid)
      if (block && block.kind === 'text' && block.data.match(userRegex)) {
        return true
      }
    }
    return false
  }

  hasContent() {
    const { order } = this.state
    const firstBlock = this.getBlockFromUid(order[0])
    if (!firstBlock) { return false }
    return (
      order.length > 1 ||
      firstBlock &&
      firstBlock.data.length &&
      firstBlock.data !== '
'
    )
  }

  render() {
    const { avatar, cancelAction, editorId, isComment, submitText } = this.props
    const { dragBlockTop, order } = this.state
    const hasMention = this.hasMention()
    const hasContent = this.hasContent()
    return (

        { isComment ? <Avatar sources={ avatar }/> : null }

          { order.map((uid) => this.getBlockElement(this.getBlockFromUid(uid))) }
          { this.dragBlock ?

              { this.getBlockElement(this.dragBlock) }

 :
            null
          }

        { isComment ? <QuickEmoji onAddEmoji={ this.onInsertEmoji }/> : null }
        <PostActionBar
          ref="postActionBar"
          cancelAction={ cancelAction }
          editorId={ editorId }
          submitAction={ this.submit }
          submitText={ submitText }
        />

    )
  }

}

function mapStateToProps(state, ownProps) {
  return {
    avatar: state.profile.avatar,
    completions: state.editor.completions,
    editorStore: state.editor.editors[ownProps.editorId],
    emoji: state.emoji,
  }
}

export default connect(mapStateToProps)(BlockCollection)

<commit_msg>Update data with the innerHTML.<commit_after>import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import classNames from 'classnames'
import { debounce } from 'lodash'
import Avatar from '../assets/Avatar'
import Block from './Block'
import EmbedBlock from './EmbedBlock'
import ImageBlock from './ImageBlock'
import QuickEmoji from './QuickEmoji'
import RepostBlock from './RepostBlock'
import TextBlock from './TextBlock'
import PostActionBar from './PostActionBar'
import * as ACTION_TYPES from '../../constants/action_types'
import { addDragObject, removeDragObject } from './DragComponent'
import { addInputObject, removeInputObject } from './InputComponent'
import { userRegex } from '../completers/Completer'

class BlockCollection extends Component {

  static propTypes = {
    avatar: PropTypes.object.isRequired,
    blocks: PropTypes.array,
    cancelAction: PropTypes.func.isRequired,
    completions: PropTypes.shape({
      data: PropTypes.array,
      type: PropTypes.string,
    }),
    dispatch: PropTypes.func.isRequired,
    editorId: PropTypes.string.isRequired,
    editorStore: PropTypes.object,
    emoji: PropTypes.object.isRequired,
    isComment: PropTypes.bool,
    repostContent: PropTypes.array,
    shouldLoadFromState: PropTypes.bool,
    shouldPersist: PropTypes.bool,
    submitAction: PropTypes.func.isRequired,
    submitText: PropTypes.string,
  };

  static defaultProps = {
    blocks: [],
    editorStore: {},
    isComment: false,
    repostContent: [],
    shouldLoadFromState: false,
    shouldPersist: false,
    submitText: 'Post',
  };

  componentWillMount() {
    const { blocks, editorStore, repostContent, shouldLoadFromState } = this.props
    this.state = {
      collection: {},
      hideTextTools: true,
      order: [],
    }
    this.uid = 0

    if (repostContent.length) {
      this.add({ kind: 'repost', data: repostContent })
    }
    if (blocks.length) {
      for (const block of blocks) {
        this.add(block, false)
      }
    } else if (shouldLoadFromState && editorStore.editorState) {
      this.state = { ...editorStore.editorState, hideTextTools: true }
      this.uid = Math.max(...editorStore.editorState.order) + 1
    }
    this.persistBlocks = debounce(this.persistBlocks, 300)
  }

  componentDidMount() {
    const { editorId } = this.props
    // TODO: figure out why this is causing a setState warning
    // you can get the error to happen by going to a profile page
    // visiting a post detail and then hitting back, the setState
    // happens in add, but is triggered from this method since
    // commenting it out gets rid of the issue
    this.addEmptyTextBlock()
    addDragObject({ component: this, dragId: editorId })
    addInputObject(this)
  }

  componentWillReceiveProps(nextProps) {
    const { dispatch, editorId, editorStore } = nextProps
    const { collection } = this.state
    let newBlock = null
    switch (editorStore.type) {
      case ACTION_TYPES.EDITOR.APPEND_TEXT:
        if (editorStore.appendText && editorStore.appendText.length) {
          this.appendText(editorStore.appendText)
          dispatch({ type: ACTION_TYPES.EDITOR.CLEAR_APPENDED_TEXT, payload: { editorId } })
        }
        break
      case ACTION_TYPES.POST.TMP_IMAGE_CREATED:
        this.removeEmptyTextBlock()
        newBlock = this.add({ kind: 'image', data: { url: editorStore.url } })
        dispatch({
          type: ACTION_TYPES.POST.IMAGE_BLOCK_CREATED,
          payload: {
            editorId,
            uid: newBlock.uid,
          },
        })
        break
      case ACTION_TYPES.POST.SAVE_IMAGE_SUCCESS:
        newBlock = this.getBlockFromUid(editorStore.uid)
        if (newBlock) {
          collection[this.getBlockIdentifier(editorStore.uid)] = {
            kind: 'image',
            data: {
              url: editorStore.url,
            },
            uid: editorStore.uid,
          }
          this.setState({ collection })
          this.persistBlocks()
        }
        break
      case ACTION_TYPES.POST.POST_PREVIEW_SUCCESS:
        this.removeEmptyTextBlock()
        this.add({ ...editorStore.postPreviews.body[0] })
        break
      default:
        break
    }
  }

  componentDidUpdate(prevProps) {
    const { completions } = this.props
    const prevCompletions = prevProps.completions
    if (prevCompletions && !completions) {
      requestAnimationFrame(() => {
        this.updateTextCollectionData()
      })
    }
  }

  componentWillUnmount() {
    // this.onHideCompleter()
    removeDragObject(this)
    removeInputObject(this)
  }

  onDragStart(props) {
    const { collection } = this.state
    this.blockNode = props.target.parentNode.parentNode
    this.startOffset = this.blockNode.offsetTop
    this.startHeight = this.blockNode.offsetHeight
    this.prevBlock = this.blockNode.previousSibling
    this.nextBlock = this.blockNode.nextSibling
    const dragUid = this.blockNode.dataset.collectionId
    this.dragBlock = collection[this.getBlockIdentifier(dragUid)]
    // swap the dragging block for a
    // normal block and set the height/width
    collection[this.getBlockIdentifier(dragUid)] = {
      data: {
        width: this.blockNode.offsetWidth,
        height: this.blockNode.offsetHeight,
      },
      kind: 'block',
      uid: this.dragBlock.uid,
    }
    this.onDragMove(props)
    this.setState({ collection })
  }

  onDragMove(props) {
    // move the block we are currently dragging
    this.setState({ dragBlockTop: props.dragY + this.startOffset })
    // determine if we should change order
    if (props.dragY < props.lastDragY) {
      this.onDragUp(props)
    } else if (props.dragY > props.lastDragY) {
      this.onDragDown(props)
    }
  }

  onDragUp(props) {
    if (this.prevBlock &&
        !this.prevBlock.classList.contains('readonly') &&
        (props.dragY + this.startOffset) <
        (this.prevBlock.offsetTop + this.prevBlock.offsetHeight * 0.5)) {
      this.onMoveBlock(-1)
    }
  }

  onDragDown(props) {
    if (this.nextBlock &&
        (props.dragY + this.startOffset + this.startHeight) >
        (this.nextBlock.offsetTop + this.nextBlock.offsetHeight * 0.5)) {
      this.onMoveBlock(1)
    }
  }

  onMoveBlock(delta) {
    if (!this.refs.blockPlaceholder) return
    const { order } = this.state
    const dragUid = this.dragBlock.uid
    const index = order.indexOf(dragUid)
    // remove from old spot
    order.splice(index, 1)
    // add to new spot
    order.splice(index + delta, 0, dragUid)
    this.setState({ order })
    const placeholder = this.refs.blockPlaceholder.refs.editorBlock
    // update prev/next blocks
    this.prevBlock = placeholder.previousSibling
    this.nextBlock = placeholder.nextSibling
  }

  onDragEnd() {
    const { collection } = this.state
    // swap the normal block out for
    // the one that was removed initially
    const dragUid = this.dragBlock.uid
    collection[this.getBlockIdentifier(dragUid)] = this.dragBlock
    // order matters here so the dragBlock gets removed
    this.dragBlock = null
    this.setState({ collection, dragBlockTop: null })
    this.addEmptyTextBlock(true)
  }

  onSubmitPost() {
    const { editorId } = this.props
    if (document.activeElement.parentNode.dataset.editorId === editorId) {
      this.submit()
    }
  }

  onInsertEmoji = ({ value }) => {
    this.appendText(value)
  };

  getBlockIdentifier(uid) {
    const { editorId } = this.props
    return `${editorId}_${uid}`
  }

  getBlockFromUid(uid) {
    const { collection } = this.state
    return collection[this.getBlockIdentifier(uid)]
  }

  getBlockElement(block) {
    const { editorId } = this.props
    const blockProps = {
      data: block.data,
      editorId,
      key: block.uid,
      kind: block.kind,
      onRemoveBlock: this.remove,
      ref: `block${block.uid}`,
      uid: block.uid,
    }
    switch (block.kind) {
      case 'block':
        return (
          <Block { ...blockProps } className="BlockPlaceholder" ref="blockPlaceholder" />
        )
      case 'embed':
        return (
          <EmbedBlock { ...blockProps } />
        )
      case 'image':
        return (
          <ImageBlock { ...blockProps } />
        )
      case 'repost':
        return (
          <RepostBlock { ...blockProps } onRemoveBlock={ null } />
        )
      case 'text':
        return (
          <TextBlock
            { ...blockProps }
            onInput={ this.handleTextBlockInput }
          />
        )
      default:
        return null
    }
  }

  // TODO: probably should have the completer
  // dispatch an action that updates the gui
  // and then we can listen to that instead
  // of brute forcing it with dom lookups
  updateTextCollectionData() {
    const { editorId } = this.props
    const { collection, order } = this.state
    for (const uid of order) {
      const block = this.getBlockFromUid(uid)
      if (block && block.kind === 'text') {
        const selector = `[data-editor-id="${editorId}"][data-collection-id="${uid}"]`
        block.data = document.querySelector(selector).firstChild.innerHTML
      }
    }
    this.setState({ collection })
  }

  add(block, shouldCheckForEmpty = true) {
    const newBlock = { ...block, uid: this.uid }
    const { collection, order } = this.state
    collection[this.getBlockIdentifier(this.uid)] = newBlock
    order.push(this.uid)
    this.uid++
    // order matters here
    this.setState({ collection, order })
    if (shouldCheckForEmpty) {
      this.addEmptyTextBlock()
    }
    this.persistBlocks()
    return newBlock
  }

  addEmptyTextBlock(shouldCheckForEmpty = false) {
    const { order } = this.state
    requestAnimationFrame(() => {
      if (order.length > 1) {
        const last = this.getBlockFromUid(order[order.length - 1])
        const secondToLast = this.getBlockFromUid(order[order.length - 2])
        if (secondToLast.kind === 'text' &&
            last.kind === 'text' && !last.data.length) {
          return this.remove(last.uid, shouldCheckForEmpty)
        }
      }
      if (!order.length ||
          this.getBlockFromUid(order[order.length - 1]).kind !== 'text') {
        this.add({ kind: 'text', data: '' }, false)
      }
    })
  }

  appendText = (content) => {
    const { collection, order } = this.state
    const textBlocks = order.filter((uid) => this.getBlockFromUid(uid).kind === 'text')
    const lastBlock = this.getBlockFromUid(textBlocks[textBlocks.length - 1])
    if (lastBlock) {
      lastBlock.data += content
      this.setState({ collection })
    }
  };

  remove = (uid, shouldCheckForEmpty = true) => {
    const { collection, order } = this.state
    delete collection[this.getBlockIdentifier(uid)]
    order.splice(order.indexOf(uid), 1)
    // order matters here
    this.setState({ collection, order })
    if (shouldCheckForEmpty) {
      this.addEmptyTextBlock()
    }
    this.persistBlocks()
  };

  persistBlocks() {
    const { dispatch, editorId, shouldPersist } = this.props
    if (!shouldPersist) { return }
    const { collection, order } = this.state
    dispatch({
      type: ACTION_TYPES.POST.PERSIST,
      payload: {
        collection,
        editorId,
        order,
      },
    })
  }

  removeEmptyTextBlock() {
    const { order } = this.state
    if (order.length > 0) {
      const last = this.getBlockFromUid(order[order.length - 1])
      if (last && last.kind === 'text' && !last.data.length) {
        this.remove(last.uid, false)
      }
    }
  }

  handleTextBlockInput = (vo) => {
    const { collection } = this.state
    collection[this.getBlockIdentifier(vo.uid)] = vo
    this.setState({ collection })
    this.persistBlocks()
  };

  submit = () => {
    const { isComment, submitAction } = this.props
    const data = this.serialize()
    submitAction(data)
    if (isComment) {
      this.clearBlocks()
    }
  };

  clearBlocks() {
    this.setState({ collection: {}, order: [] })
    requestAnimationFrame(() => {
      this.uid = 0
      this.addEmptyTextBlock()
    })
  }

  serialize() {
    const { order } = this.state
    const results = []
    for (const uid of order) {
      const block = this.getBlockFromUid(uid)
      switch (block.kind) {
        case 'text':
          if (block.data.length) {
            results.push({ kind: block.kind, data: block.data })
          }
          break
        case 'repost':
          break
        default:
          results.push({ kind: block.kind, data: block.data })
          break
      }
    }
    return results
  }

  hasMention() {
    const { order } = this.state
    for (const uid of order) {
      const block = this.getBlockFromUid(uid)
      if (block && block.kind === 'text' && block.data.match(userRegex)) {
        return true
      }
    }
    return false
  }

  hasContent() {
    const { order } = this.state
    const firstBlock = this.getBlockFromUid(order[0])
    if (!firstBlock) { return false }
    return (
      order.length > 1 ||
      firstBlock &&
      firstBlock.data.length &&
      firstBlock.data !== '
'
    )
  }

  render() {
    const { avatar, cancelAction, editorId, isComment, submitText } = this.props
    const { dragBlockTop, order } = this.state
    const hasMention = this.hasMention()
    const hasContent = this.hasContent()
    return (

        { isComment ? <Avatar sources={ avatar }/> : null }

          { order.map((uid) => this.getBlockElement(this.getBlockFromUid(uid))) }
          { this.dragBlock ?

              { this.getBlockElement(this.dragBlock) }

 :
            null
          }

        { isComment ? <QuickEmoji onAddEmoji={ this.onInsertEmoji }/> : null }
        <PostActionBar
          ref="postActionBar"
          cancelAction={ cancelAction }
          editorId={ editorId }
          submitAction={ this.submit }
          submitText={ submitText }
        />

    )
  }

}

function mapStateToProps(state, ownProps) {
  return {
    avatar: state.profile.avatar,
    completions: state.editor.completions,
    editorStore: state.editor.editors[ownProps.editorId],
    emoji: state.emoji,
  }
}

export default connect(mapStateToProps)(BlockCollection)

<|endoftext|>


<commit_before>import { webFrame } from "electron"
import { action, computed, observable } from "mobx"
import { trim } from "ramda"
import { isNilOrEmpty } from "ramdasauce"
import Keystroke from "../Lib/Keystroke"
import Mousetrap from "../Lib/Mousetrap.min.js"

/**
 * Handles UI state.
 */
class UI {
  @observable tab = "timeline"
  @observable homeSubNav = "connections"
  @observable stateSubNav = "subscriptions"
  @observable nativeSubNav = "image"
  @observable keysOrValues = "keys"
  @observable showStateFindDialog = false
  @observable showStateDispatchDialog = false
  @observable showHelpDialog = false
  @observable showStateWatchDialog = false
  @observable showFilterTimelineDialog = false
  @observable statusBarExpanded = false
  @observable watchToAdd
  @observable actionToDispatch
  @observable showWatchPanel = false
  @observable customMessage = ""
  @observable showSendCustomDialog = false
  @observable isTimelineSearchVisible = false
  @observable isTimelineOrderReversed = false
  @observable isSidebarVisible = true
  @observable isStorybookShown = false
  @observable searchPhrase = ""
  zoomLevel = 0

  // additional properties that some commands may want... a way to communicate
  // from the command toolbar to the command
  commandProperties = {}

  constructor(server, commandsManager, stateBackupStore, getSelectedConnection) {
    this.server = server
    this.commandsManager = commandsManager
    this.stateBackupStore = stateBackupStore
    this.getSelectedConnection = getSelectedConnection

    Mousetrap.prototype.stopCallback = () => false

    Mousetrap.bind(`${Keystroke.mousetrap}+backspace`, this.reset)
    Mousetrap.bind(`${Keystroke.mousetrap}+k`, this.openStateFindDialog)
    Mousetrap.bind(`${Keystroke.mousetrap}+shift+f`, this.openFilterTimelineDialog)
    Mousetrap.bind(`${Keystroke.mousetrap}+d`, this.openStateDispatchDialog)
    Mousetrap.bind(`${Keystroke.mousetrap}+s`, () => this.stateBackupStore.sendBackup())
    Mousetrap.bind(`tab`, this.toggleKeysValues)
    Mousetrap.bind(`escape`, this.popState)
    Mousetrap.bind(`enter`, this.submitCurrentForm)
    Mousetrap.bind(`${Keystroke.mousetrap}+enter`, this.submitCurrentFormDelicately)
    Mousetrap.bind(`${Keystroke.mousetrap}+n`, this.openStateWatchDialog)
    Mousetrap.bind(`${Keystroke.mousetrap}+1`, this.switchTab.bind(this, "home"))
    Mousetrap.bind(`${Keystroke.mousetrap}+2`, this.switchTab.bind(this, "timeline"))
    Mousetrap.bind(`${Keystroke.mousetrap}+3`, this.switchTab.bind(this, "state"))
    Mousetrap.bind(`${Keystroke.mousetrap}+4`, this.switchTab.bind(this, "native"))
    Mousetrap.bind(`${Keystroke.mousetrap}+?`, this.switchTab.bind(this, "help"))
    Mousetrap.bind(`${Keystroke.mousetrap}+f`, this.showTimelineSearch)
    Mousetrap.bind(`${Keystroke.mousetrap}+.`, this.openSendCustomDialog)
    Mousetrap.bind(`${Keystroke.mousetrap}+shift+s`, this.toggleSidebar)
    Mousetrap.bind(`${Keystroke.mousetrap}+-`, this.zoomOut.bind(this))
    Mousetrap.bind(`${Keystroke.mousetrap}+=`, this.zoomIn.bind(this))
    Mousetrap.bind(`${Keystroke.mousetrap}+0`, this.resetZoom.bind(this))
  }

  zoomOut() {
    this.zoomLevel--
    webFrame.setZoomLevel(this.zoomLevel)
  }

  zoomIn() {
    this.zoomLevel++
    webFrame.setZoomLevel(this.zoomLevel)
  }

  resetZoom() {
    this.zoomLevel = 0
    webFrame.setZoomLevel(this.zoomLevel)
  }

  @action
  setSearchPhrase = value => {
    this.searchPhrase = value
  }

  /**
   * Get the scrubbed search phrase.
   */
  @computed
  get cleanedSearchPhrase() {
    return trim(this.searchPhrase || "")
  }

  /**
   * The regular expression we will be using to search commands.
   */
  @computed
  get searchRegexp() {
    try {
      return new RegExp(this.cleanedSearchPhrase.replace(/\s/, "."), "i")
    } catch (e) {
      return null
    }
  }

  /**
   * Is this search phrase useable?
   */
  @computed
  get isValidSearchPhrase() {
    return Boolean(this.searchRegexp)
  }

  @action
  hideTimelineSearch = () => {
    this.isTimelineSearchVisible = false
  }

  @action
  showTimelineSearch = () => {
    this.isTimelineSearchVisible = false // hack to ensure the reaction on the timeline header works (sheesh.)
    this.isTimelineSearchVisible = true
    this.switchTab("timeline")
  }

  @action
  toggleTimelineSearch = () => {
    if (this.isTimelineSearchVisible) {
      this.hideTimelineSearch()
    } else {
      this.showTimelineSearch()
    }
  }

  @action
  toggleTimelineOrder = () => {
    this.isTimelineOrderReversed = !this.isTimelineOrderReversed
  }

  @action
  hideSidebar = () => {
    this.isSidebarVisible = false
  }

  @action
  showSidebar = () => {
    this.isSidebarVisible = true
  }

  @action
  toggleSidebar = () => {
    if (this.isSidebarVisible) {
      this.hideSidebar()
    } else {
      this.showSidebar()
    }
  }

  @action
  switchTab = newTab => {
    this.tab = newTab
  }

  @action
  setHomeSubNav = value => {
    this.homeSubNav = value
  }

  @action
  setStateSubNav = value => {
    this.stateSubNav = value
  }

  @action
  setNativeSubNav = value => {
    this.nativeSubNav = value
  }

  @computed
  get isModalShowing() {
    return (
      this.showStateWatchDialog ||
      this.showStateFindDialog ||
      this.showStateDispatchDialog ||
      this.showFilterTimelineDialog ||
      this.stateBackupStore.renameDialogVisible ||
      this.showSendCustomDialog
    )
  }

  @action
  popState = e => {
    if (this.showStateFindDialog) {
      this.closeStateFindDialog()
    } else if (this.showHelpDialog) {
      this.closeHelpDialog()
    }
    return false
  }

  @action
  submitCurrentForm = () => {
    if (this.showStateWatchDialog) {
      this.submitStateWatch()
    } else if (this.stateBackupStore.renameDialogVisible) {
      this.stateBackupStore.commitRename()
    } else if (this.showSendCustomDialog) {
      this.submitCurrentMessage()
    }
  }

  @action
  submitCurrentFormDelicately = () => {
    if (this.showStateDispatchDialog) {
      this.submitStateDispatch()
    }
  }

  @action
  submitStateWatch = () => {
    this.server.stateValuesSubscribe(this.watchToAdd)
    this.showStateWatchDialog = false
    this.watchToAdd = null
  }

  @action
  submitCurrentMessage = () => {
    this.sendCustomMessage(this.customMessage)
    this.customMessage = ""
    this.showSendCustomDialog = false
  }

  @action
  removeStateWatch = path => {
    this.server.stateValuesUnsubscribe(path)
  }

  @action
  clearStateWatches = () => {
    this.server.stateValuesClearSubscriptions()
  }

  @action
  setActionToDispatch(action) {
    this.actionToDispatch = action
    this.showStateDispatchDialog = true
  }

  @action
  submitStateDispatch = () => {
    // try not to blow up the frame
    let action = null
    try {
      // brackets are need on chromium side, huh.
      action = eval("(" + this.actionToDispatch + ")") // eslint-disable-line
    } catch (e) {}
    // jet if not valid
    if (isNilOrEmpty(action)) return

    // let's attempt to dispatch
    this.dispatchAction(action)
    // close the form
    this.showStateDispatchDialog = false
  }

  @action
  openStateFindDialog = () => {
    this.showStateFindDialog = true
  }

  @action
  closeStateFindDialog = () => {
    this.showStateFindDialog = false
  }

  @action
  openStateWatchDialog = () => {
    this.showStateWatchDialog = true
  }

  @action
  closeStateWatchDialog = () => {
    this.showStateWatchDialog = false
  }

  @action
  openStateDispatchDialog = () => {
    this.showStateDispatchDialog = true
  }

  @action
  toggleHelpDialog = () => {
    this.showHelpDialog = !this.showHelpDialog
  }

  @action
  openHelpDialog = () => {
    this.showHelpDialog = true
  }

  @action
  closeStateDispatchDialog = () => {
    this.showStateDispatchDialog = false
  }

  @action
  closeHelpDialog = () => {
    this.showHelpDialog = false
  }

  @action
  openFilterTimelineDialog = () => {
    this.showFilterTimelineDialog = true
  }

  @action
  closeFilterTimelineDialog = () => {
    this.showFilterTimelineDialog = false
  }

  @action
  openSendCustomDialog = () => {
    this.showSendCustomDialog = true
  }

  @action
  closeSendCustomDialog = () => {
    this.showSendCustomDialog = false
  }

  @action
  openStatusBar = () => {
    this.statusBarExpanded = true
  }

  @action
  closeStatusBar = () => {
    this.statusBarExpanded = false
  }

  @action
  reset = () => {
    const selectedConnection = this.getSelectedConnection()

    if (!selectedConnection) return

    this.commandsManager.clearClientsCommands(selectedConnection.clientId)
  }

  @action
  getStateKeysOrValues = path => {
    if (this.keysOrValues === "keys") {
      this.getStateKeys(path)
    } else {
      this.getStateValues(path)
    }
  }

  @action
  getStateValues = path => {
    this.server.send("state.values.request", { path })
  }

  @action
  getStateKeys = path => {
    this.server.send("state.keys.request", { path })
  }

  @action
  dispatchAction = action => {
    this.server.send("state.action.dispatch", { action })
  }

  @action
  sendCustomMessage = value => {
    const selectedConnection = this.getSelectedConnection()

    if (!selectedConnection) return

    this.server.sendCustomMessage(value, selectedConnection.clientId)
  }

  @action
  setCustomMessage = value => {
    this.customMessage = value
  }

  @action
  toggleKeysValues = () => {
    if (this.keysOrValues === "keys") {
      this.keysOrValues = "values"
    } else {
      this.keysOrValues = "keys"
    }
  }

  @action
  toggleWatchPanel = () => {
    this.showWatchPanel = !this.showWatchPanel
  }

  getCommandProperty = (messageId, key) => {
    const props = this.commandProperties[messageId]
    if (props) {
      return props.get(key)
    } else {
      this.commandProperties[messageId] = observable.map({})
      return this.commandProperties[messageId].get(key, null)
    }
  }

  @action
  setCommandProperty = (messageId, key, value) => {
    if (!this.commandProperties[messageId]) {
      this.commandProperties[messageId] = observable.map({})
    }
    this.commandProperties[messageId].set(key, value)
  }

  /**
   * Asks the client to the file in the editor
   */
  @action openInEditor = (file, lineNumber) => this.server.send("editor.open", { file, lineNumber })

  /**
   * Sets the properties of the overlay shown on the React Native app.
   */
  @action setOverlay = props => this.server.send("overlay", props)

  /**
   * Toggles storybook
   */
  @action
  toggleStorybook = () => {
    this.isStorybookShown = !this.isStorybookShown

    this.server.send("storybook", this.isStorybookShown)
  }

  /**
   * Turn on storybook for the current client.
   */
  @action
  enableStorybook = () => {
    if (this.isStorybookShown) return
    this.isStorybookShown = true
    this.sendStorybookState()
  }

  /**
   * Turn off storybook for the current client.
   */
  @action
  disableStorybook = () => {
    if (!this.isStorybookShown) return
    this.isStorybookShown = false
    this.sendStorybookState()
  }

  /**
   * Sends the current storybook state to the app.
   */
  sendStorybookState = clientId => {
    this.server.send("storybook", this.isStorybookShown, clientId)
  }

}

export default UI
<commit_msg>:sparkles: save isTimelineOrderReversed in localstorage<commit_after>import { webFrame } from "electron"
import { action, computed, observable } from "mobx"
import { trim } from "ramda"
import { isNilOrEmpty } from "ramdasauce"
import Keystroke from "../Lib/Keystroke"
import Mousetrap from "../Lib/Mousetrap.min.js"

/**
 * Handles UI state.
 */
class UI {
  @observable tab = "timeline"
  @observable homeSubNav = "connections"
  @observable stateSubNav = "subscriptions"
  @observable nativeSubNav = "image"
  @observable keysOrValues = "keys"
  @observable showStateFindDialog = false
  @observable showStateDispatchDialog = false
  @observable showHelpDialog = false
  @observable showStateWatchDialog = false
  @observable showFilterTimelineDialog = false
  @observable statusBarExpanded = false
  @observable watchToAdd
  @observable actionToDispatch
  @observable showWatchPanel = false
  @observable customMessage = ""
  @observable showSendCustomDialog = false
  @observable isTimelineSearchVisible = false
  @observable isTimelineOrderReversed = JSON.parse(localStorage.getItem("isTimelineOrderReversed")) || false
  @observable isSidebarVisible = true
  @observable isStorybookShown = false
  @observable searchPhrase = ""
  zoomLevel = 0

  // additional properties that some commands may want... a way to communicate
  // from the command toolbar to the command
  commandProperties = {}

  constructor(server, commandsManager, stateBackupStore, getSelectedConnection) {
    this.server = server
    this.commandsManager = commandsManager
    this.stateBackupStore = stateBackupStore
    this.getSelectedConnection = getSelectedConnection

    Mousetrap.prototype.stopCallback = () => false

    Mousetrap.bind(`${Keystroke.mousetrap}+backspace`, this.reset)
    Mousetrap.bind(`${Keystroke.mousetrap}+k`, this.openStateFindDialog)
    Mousetrap.bind(`${Keystroke.mousetrap}+shift+f`, this.openFilterTimelineDialog)
    Mousetrap.bind(`${Keystroke.mousetrap}+d`, this.openStateDispatchDialog)
    Mousetrap.bind(`${Keystroke.mousetrap}+s`, () => this.stateBackupStore.sendBackup())
    Mousetrap.bind(`tab`, this.toggleKeysValues)
    Mousetrap.bind(`escape`, this.popState)
    Mousetrap.bind(`enter`, this.submitCurrentForm)
    Mousetrap.bind(`${Keystroke.mousetrap}+enter`, this.submitCurrentFormDelicately)
    Mousetrap.bind(`${Keystroke.mousetrap}+n`, this.openStateWatchDialog)
    Mousetrap.bind(`${Keystroke.mousetrap}+1`, this.switchTab.bind(this, "home"))
    Mousetrap.bind(`${Keystroke.mousetrap}+2`, this.switchTab.bind(this, "timeline"))
    Mousetrap.bind(`${Keystroke.mousetrap}+3`, this.switchTab.bind(this, "state"))
    Mousetrap.bind(`${Keystroke.mousetrap}+4`, this.switchTab.bind(this, "native"))
    Mousetrap.bind(`${Keystroke.mousetrap}+?`, this.switchTab.bind(this, "help"))
    Mousetrap.bind(`${Keystroke.mousetrap}+f`, this.showTimelineSearch)
    Mousetrap.bind(`${Keystroke.mousetrap}+.`, this.openSendCustomDialog)
    Mousetrap.bind(`${Keystroke.mousetrap}+shift+s`, this.toggleSidebar)
    Mousetrap.bind(`${Keystroke.mousetrap}+-`, this.zoomOut.bind(this))
    Mousetrap.bind(`${Keystroke.mousetrap}+=`, this.zoomIn.bind(this))
    Mousetrap.bind(`${Keystroke.mousetrap}+0`, this.resetZoom.bind(this))
  }

  zoomOut() {
    this.zoomLevel--
    webFrame.setZoomLevel(this.zoomLevel)
  }

  zoomIn() {
    this.zoomLevel++
    webFrame.setZoomLevel(this.zoomLevel)
  }

  resetZoom() {
    this.zoomLevel = 0
    webFrame.setZoomLevel(this.zoomLevel)
  }

  @action
  setSearchPhrase = value => {
    this.searchPhrase = value
  }

  /**
   * Get the scrubbed search phrase.
   */
  @computed
  get cleanedSearchPhrase() {
    return trim(this.searchPhrase || "")
  }

  /**
   * The regular expression we will be using to search commands.
   */
  @computed
  get searchRegexp() {
    try {
      return new RegExp(this.cleanedSearchPhrase.replace(/\s/, "."), "i")
    } catch (e) {
      return null
    }
  }

  /**
   * Is this search phrase useable?
   */
  @computed
  get isValidSearchPhrase() {
    return Boolean(this.searchRegexp)
  }

  @action
  hideTimelineSearch = () => {
    this.isTimelineSearchVisible = false
  }

  @action
  showTimelineSearch = () => {
    this.isTimelineSearchVisible = false // hack to ensure the reaction on the timeline header works (sheesh.)
    this.isTimelineSearchVisible = true
    this.switchTab("timeline")
  }

  @action
  toggleTimelineSearch = () => {
    if (this.isTimelineSearchVisible) {
      this.hideTimelineSearch()
    } else {
      this.showTimelineSearch()
    }
  }

  @action
  toggleTimelineOrder = () => {
    this.isTimelineOrderReversed = !this.isTimelineOrderReversed
    localStorage.setItem("isTimelineOrderReversed", JSON.stringify(this.isTimelineOrderReversed))
  }

  @action
  hideSidebar = () => {
    this.isSidebarVisible = false
  }

  @action
  showSidebar = () => {
    this.isSidebarVisible = true
  }

  @action
  toggleSidebar = () => {
    if (this.isSidebarVisible) {
      this.hideSidebar()
    } else {
      this.showSidebar()
    }
  }

  @action
  switchTab = newTab => {
    this.tab = newTab
  }

  @action
  setHomeSubNav = value => {
    this.homeSubNav = value
  }

  @action
  setStateSubNav = value => {
    this.stateSubNav = value
  }

  @action
  setNativeSubNav = value => {
    this.nativeSubNav = value
  }

  @computed
  get isModalShowing() {
    return (
      this.showStateWatchDialog ||
      this.showStateFindDialog ||
      this.showStateDispatchDialog ||
      this.showFilterTimelineDialog ||
      this.stateBackupStore.renameDialogVisible ||
      this.showSendCustomDialog
    )
  }

  @action
  popState = e => {
    if (this.showStateFindDialog) {
      this.closeStateFindDialog()
    } else if (this.showHelpDialog) {
      this.closeHelpDialog()
    }
    return false
  }

  @action
  submitCurrentForm = () => {
    if (this.showStateWatchDialog) {
      this.submitStateWatch()
    } else if (this.stateBackupStore.renameDialogVisible) {
      this.stateBackupStore.commitRename()
    } else if (this.showSendCustomDialog) {
      this.submitCurrentMessage()
    }
  }

  @action
  submitCurrentFormDelicately = () => {
    if (this.showStateDispatchDialog) {
      this.submitStateDispatch()
    }
  }

  @action
  submitStateWatch = () => {
    this.server.stateValuesSubscribe(this.watchToAdd)
    this.showStateWatchDialog = false
    this.watchToAdd = null
  }

  @action
  submitCurrentMessage = () => {
    this.sendCustomMessage(this.customMessage)
    this.customMessage = ""
    this.showSendCustomDialog = false
  }

  @action
  removeStateWatch = path => {
    this.server.stateValuesUnsubscribe(path)
  }

  @action
  clearStateWatches = () => {
    this.server.stateValuesClearSubscriptions()
  }

  @action
  setActionToDispatch(action) {
    this.actionToDispatch = action
    this.showStateDispatchDialog = true
  }

  @action
  submitStateDispatch = () => {
    // try not to blow up the frame
    let action = null
    try {
      // brackets are need on chromium side, huh.
      action = eval("(" + this.actionToDispatch + ")") // eslint-disable-line
    } catch (e) {}
    // jet if not valid
    if (isNilOrEmpty(action)) return

    // let's attempt to dispatch
    this.dispatchAction(action)
    // close the form
    this.showStateDispatchDialog = false
  }

  @action
  openStateFindDialog = () => {
    this.showStateFindDialog = true
  }

  @action
  closeStateFindDialog = () => {
    this.showStateFindDialog = false
  }

  @action
  openStateWatchDialog = () => {
    this.showStateWatchDialog = true
  }

  @action
  closeStateWatchDialog = () => {
    this.showStateWatchDialog = false
  }

  @action
  openStateDispatchDialog = () => {
    this.showStateDispatchDialog = true
  }

  @action
  toggleHelpDialog = () => {
    this.showHelpDialog = !this.showHelpDialog
  }

  @action
  openHelpDialog = () => {
    this.showHelpDialog = true
  }

  @action
  closeStateDispatchDialog = () => {
    this.showStateDispatchDialog = false
  }

  @action
  closeHelpDialog = () => {
    this.showHelpDialog = false
  }

  @action
  openFilterTimelineDialog = () => {
    this.showFilterTimelineDialog = true
  }

  @action
  closeFilterTimelineDialog = () => {
    this.showFilterTimelineDialog = false
  }

  @action
  openSendCustomDialog = () => {
    this.showSendCustomDialog = true
  }

  @action
  closeSendCustomDialog = () => {
    this.showSendCustomDialog = false
  }

  @action
  openStatusBar = () => {
    this.statusBarExpanded = true
  }

  @action
  closeStatusBar = () => {
    this.statusBarExpanded = false
  }

  @action
  reset = () => {
    const selectedConnection = this.getSelectedConnection()

    if (!selectedConnection) return

    this.commandsManager.clearClientsCommands(selectedConnection.clientId)
  }

  @action
  getStateKeysOrValues = path => {
    if (this.keysOrValues === "keys") {
      this.getStateKeys(path)
    } else {
      this.getStateValues(path)
    }
  }

  @action
  getStateValues = path => {
    this.server.send("state.values.request", { path })
  }

  @action
  getStateKeys = path => {
    this.server.send("state.keys.request", { path })
  }

  @action
  dispatchAction = action => {
    this.server.send("state.action.dispatch", { action })
  }

  @action
  sendCustomMessage = value => {
    const selectedConnection = this.getSelectedConnection()

    if (!selectedConnection) return

    this.server.sendCustomMessage(value, selectedConnection.clientId)
  }

  @action
  setCustomMessage = value => {
    this.customMessage = value
  }

  @action
  toggleKeysValues = () => {
    if (this.keysOrValues === "keys") {
      this.keysOrValues = "values"
    } else {
      this.keysOrValues = "keys"
    }
  }

  @action
  toggleWatchPanel = () => {
    this.showWatchPanel = !this.showWatchPanel
  }

  getCommandProperty = (messageId, key) => {
    const props = this.commandProperties[messageId]
    if (props) {
      return props.get(key)
    } else {
      this.commandProperties[messageId] = observable.map({})
      return this.commandProperties[messageId].get(key, null)
    }
  }

  @action
  setCommandProperty = (messageId, key, value) => {
    if (!this.commandProperties[messageId]) {
      this.commandProperties[messageId] = observable.map({})
    }
    this.commandProperties[messageId].set(key, value)
  }

  /**
   * Asks the client to the file in the editor
   */
  @action openInEditor = (file, lineNumber) => this.server.send("editor.open", { file, lineNumber })

  /**
   * Sets the properties of the overlay shown on the React Native app.
   */
  @action setOverlay = props => this.server.send("overlay", props)

  /**
   * Toggles storybook
   */
  @action
  toggleStorybook = () => {
    this.isStorybookShown = !this.isStorybookShown

    this.server.send("storybook", this.isStorybookShown)
  }

  /**
   * Turn on storybook for the current client.
   */
  @action
  enableStorybook = () => {
    if (this.isStorybookShown) return
    this.isStorybookShown = true
    this.sendStorybookState()
  }

  /**
   * Turn off storybook for the current client.
   */
  @action
  disableStorybook = () => {
    if (!this.isStorybookShown) return
    this.isStorybookShown = false
    this.sendStorybookState()
  }

  /**
   * Sends the current storybook state to the app.
   */
  sendStorybookState = clientId => {
    this.server.send("storybook", this.isStorybookShown, clientId)
  }

}

export default UI
<|endoftext|>


<commit_before>/*
 * @copyright Copyright (c) Sematext Group, Inc. - All Rights Reserved
 *
 * @licence SPM for NodeJS is free-to-use, proprietary software.
 * THIS IS PROPRIETARY SOURCE CODE OF Sematext Group, Inc. (Sematext)
 * This source code may not be copied, reverse engineered, or altered for any purpose.
 * This source code is to be used exclusively by users and customers of Sematext.
 * Please see the full license (found in LICENSE in this distribution) for details on its license and the licenses of its dependencies.
 */

'use strict'
var moment = require('moment')
var logger = require('./util/logger.js')
function formatArray (a) {
  if (a instanceof Array) {
    return a.join('\t')
  } else {
    return a + ''
  }
}
/**
 * Agent super class that provides an EventEmitter for 'metric' event.
 * In addition it fires for each added metric an event named by the property metric.name
 *
 * @param {function} plugin -  Function providing a plugin having start(agent) and stop method. The plugin can use agent.addMetric function.
 * @returns {function} an Agent having start,stop, addMetric function
 */
function Agent (plugin) {
  var events = require('events')
  var util = require('util')
  var eventEmitter = new events.EventEmitter()
  var cluster = require('cluster')
  var workerId = 0 + '-' + process.pid // 0 == Master, default
  if (!cluster.isMaster) {
    workerId = cluster.worker.id + '-' + process.pid
  }
  var agentSuperClass = {
    plugin: plugin,
    metricBuffer: [],
    /**
     * Adds a metric value and emits event to listeners on 'metric' or metric.name
     * @param metric
     */
    addMetrics: function (metric) {
      metric.workerId = workerId
      metric.pid = process.pid
      if (!metric.type)
      {
        metric.type = 'njs'
      }
      if (!metric.ts) {
        metric.ts = new Date().getTime()
      }
      metric.spmLine = this.formatLine (metric)
      this.emit('metric', metric)
      if (metric.name) {
        this.emit(metric.name, metric)
      }
    },

    defaultFormatLine: function (metric) {
      var now = (new Date().getTime()).toFixed(0)
      var metricsTs = (metric.ts).toFixed(0)
      var now2 = (new Date().getTime() / 1000).toFixed(0)
      var metricsTs2 = (metric.ts / 1000).toFixed(0)
      var dateString = moment(metric.ts).format('YYYY-MM-DD')
      var line = null
      if (metric.sct === 'OS') {
        if ((/collectd\-/.test(metric.name))) {
          if (/collectd\-cpu/.test(metric.name)) {
            line = (now2 + '\t' + metric.name + '-' + dateString + ',' + metricsTs2 + ',' + metric.value)
          } else {
            line = now2 + '\t' + metric.name + '\t' + metricsTs2 + ',' + metric.value
          }
        } else {
          line = (now / 1000).toFixed(0) + '\t' + metric.name + '\t' + metricsTs + ',' + metric.value
        }
      } else {
        line = now + '\t' + 'njs-' + metric.name + '\t' + metric.ts + '\t' + (metric.workerId || 0) + '\t' + (metric.pid || 0) + '\t' + formatArray(metric.value)
      }
      //logger.debug(line)
      return line
    },
    /**
     * formats a line for SPM sender, if the plugin does not support this method the default formatter for nodejs and OP metrics is used
     * @maram metric to format
     */
    formatLine: function (metric) {
      if(!this.plugin.formatLine)
      {
        return this.defaultFormatLine(metric)
      } else {
        return this.plugin.formatLine(metric)
      }
    },

    /**
     * Starts the agent - typically thy create listeners or interval checks for metrics, and use 'addMetrics' to inform listeners
     */
    start: function () {
      if (this.plugin && this.plugin.start) {
        // this is the place to register for external events that return metrics
        this.plugin.start(this)
      } else {
        console.log('NOT called start')
      }
    },
    /**
     * Stops the plugin - timers could be pushed to plugins 'timers' Array, if timers exists it tries to clearInterval(timerId)
     * if no timers are defined it tries to call stop method of the plugin
     */
    stop: function () {
      if (this.plugin.timers) {
        this.plugin.timers.forEach(function (tid) {
          clearInterval(tid)
        })
      } else {
        if (this.plugin.stop) {
          this.plugin.stop()
        } else {
          logger.warn('could not stop agent')
        }
      }
    }
  }
  return util._extend(eventEmitter, agentSuperClass)
}

module.exports = Agent
<commit_msg>added plugable metric type and filter values for line formatting<commit_after>/*
 * @copyright Copyright (c) Sematext Group, Inc. - All Rights Reserved
 *
 * @licence SPM for NodeJS is free-to-use, proprietary software.
 * THIS IS PROPRIETARY SOURCE CODE OF Sematext Group, Inc. (Sematext)
 * This source code may not be copied, reverse engineered, or altered for any purpose.
 * This source code is to be used exclusively by users and customers of Sematext.
 * Please see the full license (found in LICENSE in this distribution) for details on its license and the licenses of its dependencies.
 */

'use strict'
var moment = require('moment')
var logger = require('./util/logger.js')
var util = require ('util')

function formatArray (a) {
  if (a instanceof Array) {
    return a.join('\t')
  } else {
    return a + ''
  }
}
/**
 * Agent super class that provides an EventEmitter for 'metric' event.
 * In addition it fires for each added metric an event named by the property metric.name
 *
 * @param {function} plugin -  Function providing a plugin having start(agent) and stop method. The plugin can use agent.addMetric function.
 * @returns {function} an Agent having start,stop, addMetric function
 */
function Agent (plugin) {
  var events = require('events')
  var util = require('util')
  var eventEmitter = new events.EventEmitter()
  var cluster = require('cluster')
  var workerId = 0 + '-' + process.pid // 0 == Master, default
  if (!cluster.isMaster) {
    workerId = cluster.worker.id + '-' + process.pid
  }
  var agentSuperClass = {
    plugin: plugin,
    defaultType: 'njs',
    defaultFilters: function () {
      return [workerId || 0, process.pid || 0]
    },
    metricBuffer: [],
    /**
     * Adds a metric value and emits event to listeners on 'metric' or metric.name
     * @param metric
     */
    addMetrics: function (metric) {
      metric.workerId = workerId
      metric.pid = process.pid
      if (!metric.ts) {
        metric.ts = new Date().getTime()
      }
      if (!metric.type) {
        metric.type = this.defaultType
      }
      if (!metric.filters && metric.type === 'njs') {
        metric.filters = this.defaultFilters()
      }
      metric.spmLine = this.formatLine(metric)
      this.emit('metric', metric)
      if (metric.name) {
        this.emit(metric.name, metric)
      }
    },
    collectdFormatLine: function (metric) {
      var metricsTs = (metric.ts).toFixed(0)
      var now2 = (new Date().getTime() / 1000).toFixed(0)
      var metricsTs2 = (metric.ts / 1000).toFixed(0)
      var dateString = moment(metric.ts).format('YYYY-MM-DD')
      if ((/collectd\-/.test(metric.name))) {
        if (/collectd\-cpu/.test(metric.name)) {
          line = (now2 + '\t' + metric.name + '-' + dateString + ',' + metricsTs2 + ',' + metric.value)
        } else {
          line = now2 + '\t' + metric.name + '\t' + metricsTs2 + ',' + metric.value
        }
      } else {
        line = (now / 1000).toFixed(0) + '\t' + metric.name + '\t' + metricsTs + ',' + metric.value
      }
      return line
    },

    defaultFormatLine: function (metric) {
      var now = (new Date().getTime()).toFixed(0)
      var line = null
      if (metric.sct === 'OS') {
        this.collectdFormatLine(metric)
      } else {
        line = util.format("%d\t%s\t%d\t%s\t%s", now, (metric.type || this.defaultType) + '-' + metric.name, metric.ts, formatArray(metric.filters), formatArray(metric.value))
        logger.log( now + '\t' + 'njs-' + metric.name + '\t' + metric.ts + '\t' + (metric.workerId || 0) + '\t' + (metric.pid || 0) + '\t' + formatArray(metric.value) )
      }
      return line
    },
    /**
     * formats a line for SPM sender, if the plugin does not support this method the default formatter for nodejs and OP metrics is used
     * @maram metric to format
     */
    formatLine: function (metric) {
      if (!this.plugin.formatLine) {
        return this.defaultFormatLine(metric)
      } else {
        return this.plugin.formatLine(metric)
      }
    },

    /**
     * Starts the agent - typically thy create listeners or interval checks for metrics, and use 'addMetrics' to inform listeners
     */
    start: function () {
      if (this.plugin && this.plugin.start) {
        // this is the place to register for external events that return metrics
        this.plugin.start(this)
      } else {
        console.log('NOT called start')
      }
    },
    /**
     * Stops the plugin - timers could be pushed to plugins 'timers' Array, if timers exists it tries to clearInterval(timerId)
     * if no timers are defined it tries to call stop method of the plugin
     */
    stop: function () {
      if (this.plugin.timers) {
        this.plugin.timers.forEach(function (tid) {
          clearInterval(tid)
        })
      } else {
        if (this.plugin.stop) {
          this.plugin.stop()
        } else {
          logger.warn('could not stop agent')
        }
      }
    }
  }
  return util._extend(eventEmitter, agentSuperClass)
}

module.exports = Agent
<|endoftext|>


<commit_before>/*
 * @copyright Copyright (c) Sematext Group, Inc. - All Rights Reserved
 *
 * @licence SPM for NodeJS is free-to-use, proprietary software.
 * THIS IS PROPRIETARY SOURCE CODE OF Sematext Group, Inc. (Sematext)
 * This source code may not be copied, reverse engineered, or altered for any purpose.
 * This source code is to be used exclusively by users and customers of Sematext.
 * Please see the full license (found in LICENSE in this distribution) for details on its license and the licenses of its dependencies.
 */

'use strict'
var moment = require('moment')
var logger = require('./util/logger.js')

function formatArray (a) {
  if (a instanceof Array) {
    return a.join('\t')
  } else {
    return a + ''
  }
}

/**
 * Agent super class that provides an EventEmitter for 'metric' event.
 * In addition it fires for each added metric an event named by the property metric.name
 *
 * @param {function} plugin -  Function providing a plugin having start(agent) and stop method. The plugin can use agent.addMetric function.
 * @returns {function} an Agent having start,stop, addMetric function
 */
function Agent (plugin) {
  var events = require('events')
  var util = require('util')
  var eventEmitter = new events.EventEmitter()
  var cluster = require('cluster')
  var workerId = 0 + '-' + process.pid // 0 == Master, default
  var config = require('./util/spmconfig.js')
  require('./util/docker.js')
  require('./util/kubernetes.js')

  if (!cluster.isMaster) {
    workerId = cluster.worker.id + '-' + process.pid
  }
  var agentSuperClass = {
    plugin: plugin,
    defaultType: 'njs',
    defaultFilters: function () {
      return [workerId || 0, process.pid || 0]
    },
    metricBuffer: [],
    /**
     * Adds a metric value and emits event to listeners on 'metric' or metric.name
     * @param metric
     */
    addMetrics: function (metric) {
      if (metric.measurement && metric.fields) {
        return this.addInfluxMetrics(metric)
      }
      metric.workerId = workerId
      metric.pid = process.pid
      if (!metric.sct) {
        if (metric.name && /collectd/.test(metric.name)) {
          metric.sct = 'OS'
        } else {
          metric.sct = 'APP'
        }
      }
      if (!metric.ts) {
        metric.ts = new Date().getTime()
      }
      if (!metric.type) {
        metric.type = this.defaultType
      }
      if (!metric.filters && metric.type === 'njs') {
        metric.filters = this.defaultFilters()
      }
      metric.spmLine = this.formatLine(metric)
      this.emit('metric', metric)
      if (metric.name) {
        this.emit(metric.name, metric)
      }
    },

    addInfluxMetrics: function (metric) {
      if (!metric) {
        return
      }

      if (!metric.timestamp) {
        metric.timestamp = new Date()
      }
      // numeric values depend on influx-client precision settings
      // while Date objects are correctly converted by influx-client
      if (typeof metric.timestamp === 'number') {
        metric.timestamp = new Date(metric.timestamp)
      }
      this.addInfluxTags(metric)
      this.emit('metric', metric)
      if (metric.measurement) {
        this.emit(metric.measurement, metric)
      }
    },

    addInfluxTags: function (metric) {
      if (!metric.tags) {
        metric.tags = {}
      }
      // Set pid, ppid tags only for nodejs and process metrics
      if (
        metric.measurement.startsWith('nodejs') ||
        metric.measurement.startsWith('process') ||
        metric.measurement.startsWith('container')
      ) {
        // don't set default tags, when already provided
        if (!metric.tags['nodejs.worker.id']) {
          metric.tags['nodejs.worker.id'] = workerId
        }
        if (!metric.tags['process.pid']) {
          metric.tags['process.pid'] = process.pid
        }
        if (!metric.tags['process.ppid']) {
          metric.tags['process.ppid'] = process.ppid
        }

        // Set container tags only for K8s metrics if running in K8s
        console.log('\n\n-----------------------------------\n\n')
        console.log('process.env.isKubernetes: ', process.env.isKubernetes)
        console.log('process.env.kubernetesTags: ', process.env.kubernetesTags)
        const isK8s = process.env.isKubernetes === 'true'
        console.log('isK8s: ', isK8s)
        console.log('\n\n-----------------------------------\n\n')
        if (isK8s) {
          const kubernetesTags = JSON.parse(process.env.kubernetesTags)
          // don't set default tags, when already provided
          if (!metric.tags['os.host'] && kubernetesTags.os.host) {
            metric.tags['os.host'] = kubernetesTags.os.host
          }
          if (
            !metric.tags['container.id'] &&
            kubernetesTags &&
            kubernetesTags.container &&
            kubernetesTags.container.id
          ) {
            metric.tags['container.id'] = kubernetesTags.container.id
          }
          if (
            !metric.tags['container.name'] &&
            kubernetesTags &&
            kubernetesTags.container &&
            kubernetesTags.container.name
          ) {
            metric.tags['container.name'] = kubernetesTags.container.name
          }
          if (
            !metric.tags['container.image.name'] &&
            kubernetesTags &&
            kubernetesTags.image &&
            kubernetesTags.image.name
          ) {
            metric.tags['container.image.name'] = kubernetesTags.image.name
          }
          if (
            !metric.tags['container.image.tag'] &&
            kubernetesTags &&
            kubernetesTags.image &&
            kubernetesTags.image.tag
          ) {
            metric.tags['container.image.tag'] = kubernetesTags.image.tag
          }
          if (
            !metric.tags['container.image.digest'] &&
            kubernetesTags &&
            kubernetesTags.image &&
            kubernetesTags.image.digest
          ) {
            metric.tags['container.image.digest'] = kubernetesTags.image.digest
          }
          if (!metric.tags['container.pod.name'] && process.env.POD_NAME) {
            metric.tags['container.pod.name'] = process.env.POD_NAME
          }
          if (
            !metric.tags['container.pod.namespace'] &&
            process.env.POD_NAMESPACE
          ) {
            metric.tags['container.pod.namespace'] = process.env.POD_NAMESPACE
          }
        }

        // Set container tags only for container metrics if running in Docker
        const isDkr = process.env.isDocker === 'true'
        if (isDkr) {
          // don't set default tags, when already provided
          if (!metric.tags['container.id'] && process.env.containerId) {
            metric.tags['container.id'] = process.env.containerId
          }
          if (!metric.tags['container.name'] && process.env.containerName) {
            metric.tags['container.name'] = process.env.containerName
          }
          if (
            !metric.tags['container.image.name'] &&
            process.env.containerImageName
          ) {
            metric.tags['container.image.name'] = process.env.containerImageName
          }
          if (
            !metric.tags['container.image.tag'] &&
            process.env.containerImageTag
          ) {
            metric.tags['container.image.tag'] = process.env.containerImageTag
          }
          if (
            !metric.tags['container.image.digest'] &&
            process.env.containerImageDigest
          ) {
            metric.tags['container.image.digest'] =
              process.env.containerImageDigest
          }
          if (!metric.tags['swarm.stack.name'] && process.env.swarmStackName) {
            metric.tags['swarm.stack.name'] = process.env.swarmStackName
          }
          if (
            !metric.tags['swarm.service.name'] &&
            process.env.swarmServiceName
          ) {
            metric.tags['swarm.service.name'] = process.env.swarmServiceName
          }
        }
      }

      // save the metric token tag, in case global tags set 'token'
      const token = metric.tags.token
      // add global tags to metrics Object
      Object.assign(metric.tags, this.globalTags || {})
      // set metric specific token, e.g. in case of infra-app token
      if (token) {
        metric.tags.token = token
      }
    },

    collectdFormatLine: function (metric) {
      var now = new Date().getTime().toFixed(0)
      var metricsTs = metric.ts.toFixed(0)
      var metricsTs2 = (metric.ts / 1000).toFixed(0)
      var dateString = moment(metric.ts).format('YYYY-MM-DD')
      var line = ''
      if (/collectd.*-/.test(metric.name)) {
        if (
          /collectd-cpu/.test(metric.name) ||
          /collectd-io-octets/.test(metric.name)
        ) {
          line =
            now +
            '\t' +
            metric.name +
            '-' +
            dateString +
            ',' +
            metricsTs2 +
            ',' +
            metric.value
        } else if (/disk/.test(metric.name)) {
          line =
            now +
            '\t' +
            metric.name +
            '-' +
            dateString +
            ',' +
            metricsTs2 +
            ',' +
            metric.value.toFixed(6)
        } else {
          line =
            now + '\t' + metric.name + '\t' + metricsTs2 + ',' + metric.value
        }
      } else {
        line = now + '\t' + metric.name + '\t' + metricsTs + ',' + metric.value
      }
      return line
    },

    defaultFormatLine: function (metric) {
      var now = new Date().getTime().toFixed(0)
      var line = null
      if (metric.sct === 'OS') {
        line = this.collectdFormatLine(metric)
      } else {
        line = util.format(
          '%d\t%s\t%d\t%s\t%s',
          now,
          (metric.type || this.defaultType) + '-' + metric.name,
          metric.ts,
          formatArray(metric.filters),
          formatArray(metric.value)
        )
        logger.log(line)
      }
      return line
    },
    /**
     * formats a line for SPM sender, if the plugin does not support this method the default formatter for nodejs and OP metrics is used
     * @maram metric to format
     */
    formatLine: function (metric) {
      if (!this.plugin.formatLine) {
        return this.defaultFormatLine(metric)
      } else {
        return this.plugin.formatLine(metric)
      }
    },
    generateGlobalTags: function (tagNames, tagObject) {
      if (tagNames) {
        if (typeof tagNames === 'string') {
          // convert comma separated String list to array
          tagNames = tagNames.replace(/\s/g, '').split(',')
        }
        if (tagNames instanceof Array) {
          tagNames.forEach(tagName => {
            if (!tagName || tagName === '') {
              return
            }
            if (tagName.indexOf(':') > -1) {
              // static tag a=b
              const keyValue = tagName.split(':')
              if (keyValue.length === 2) {
                tagObject[keyValue[0]] = keyValue[1]
              }
            } else {
              tagObject[tagName] = process.env[tagName]
            }
          })
        }
      }
    },

    /**
     * Starts the agent - typically agents create listeners or interval checks for metrics,
     * and use 'addMetrics' to inform listeners
     */
    start: function () {
      if (!this.globalTags) {
        this.globalTags = {}
      }
      const tagNames =
        process.env.MONITORING_TAGS_FROM_ENV || config.influx.tagsFromEnv
      this.generateGlobalTags(tagNames, this.globalTags)
      if (this.plugin && this.plugin.start) {
        // this is the place to register for external events that return metrics
        this.plugin.start(this)
      } else {
        console.log('NOT called start')
      }
    },
    /**
     * Stops the plugin - timers could be pushed to plugins 'timers' Array, if timers exists it tries to clearInterval(timerId)
     * and tries to call the stop() method of the agent-plugin
     */
    stop: function () {
      if (this.plugin.timers) {
        this.plugin.timers.forEach(function (tid) {
          clearInterval(tid)
        })
      }
      if (this.plugin.stop) {
        this.plugin.stop()
      } else {
        logger.warn('Plugin ' + plugin.name + ' has no stop() function')
      }
    }
  }
  return Object.assign(eventEmitter, agentSuperClass)
}

module.exports = Agent
<commit_msg>remove comments<commit_after>/*
 * @copyright Copyright (c) Sematext Group, Inc. - All Rights Reserved
 *
 * @licence SPM for NodeJS is free-to-use, proprietary software.
 * THIS IS PROPRIETARY SOURCE CODE OF Sematext Group, Inc. (Sematext)
 * This source code may not be copied, reverse engineered, or altered for any purpose.
 * This source code is to be used exclusively by users and customers of Sematext.
 * Please see the full license (found in LICENSE in this distribution) for details on its license and the licenses of its dependencies.
 */

'use strict'
var moment = require('moment')
var logger = require('./util/logger.js')

function formatArray (a) {
  if (a instanceof Array) {
    return a.join('\t')
  } else {
    return a + ''
  }
}

/**
 * Agent super class that provides an EventEmitter for 'metric' event.
 * In addition it fires for each added metric an event named by the property metric.name
 *
 * @param {function} plugin -  Function providing a plugin having start(agent) and stop method. The plugin can use agent.addMetric function.
 * @returns {function} an Agent having start,stop, addMetric function
 */
function Agent (plugin) {
  var events = require('events')
  var util = require('util')
  var eventEmitter = new events.EventEmitter()
  var cluster = require('cluster')
  var workerId = 0 + '-' + process.pid // 0 == Master, default
  var config = require('./util/spmconfig.js')
  require('./util/docker.js')
  require('./util/kubernetes.js')

  if (!cluster.isMaster) {
    workerId = cluster.worker.id + '-' + process.pid
  }
  var agentSuperClass = {
    plugin: plugin,
    defaultType: 'njs',
    defaultFilters: function () {
      return [workerId || 0, process.pid || 0]
    },
    metricBuffer: [],
    /**
     * Adds a metric value and emits event to listeners on 'metric' or metric.name
     * @param metric
     */
    addMetrics: function (metric) {
      if (metric.measurement && metric.fields) {
        return this.addInfluxMetrics(metric)
      }
      metric.workerId = workerId
      metric.pid = process.pid
      if (!metric.sct) {
        if (metric.name && /collectd/.test(metric.name)) {
          metric.sct = 'OS'
        } else {
          metric.sct = 'APP'
        }
      }
      if (!metric.ts) {
        metric.ts = new Date().getTime()
      }
      if (!metric.type) {
        metric.type = this.defaultType
      }
      if (!metric.filters && metric.type === 'njs') {
        metric.filters = this.defaultFilters()
      }
      metric.spmLine = this.formatLine(metric)
      this.emit('metric', metric)
      if (metric.name) {
        this.emit(metric.name, metric)
      }
    },

    addInfluxMetrics: function (metric) {
      if (!metric) {
        return
      }

      if (!metric.timestamp) {
        metric.timestamp = new Date()
      }
      // numeric values depend on influx-client precision settings
      // while Date objects are correctly converted by influx-client
      if (typeof metric.timestamp === 'number') {
        metric.timestamp = new Date(metric.timestamp)
      }
      this.addInfluxTags(metric)
      this.emit('metric', metric)
      if (metric.measurement) {
        this.emit(metric.measurement, metric)
      }
    },

    addInfluxTags: function (metric) {
      if (!metric.tags) {
        metric.tags = {}
      }
      // Set pid, ppid tags only for nodejs and process metrics
      if (
        metric.measurement.startsWith('nodejs') ||
        metric.measurement.startsWith('process') ||
        metric.measurement.startsWith('container')
      ) {
        // don't set default tags, when already provided
        if (!metric.tags['nodejs.worker.id']) {
          metric.tags['nodejs.worker.id'] = workerId
        }
        if (!metric.tags['process.pid']) {
          metric.tags['process.pid'] = process.pid
        }
        if (!metric.tags['process.ppid']) {
          metric.tags['process.ppid'] = process.ppid
        }

        // Set container tags only for K8s metrics if running in K8s
        const isK8s = process.env.isKubernetes === 'true'
        if (isK8s) {
          const kubernetesTags = JSON.parse(process.env.kubernetesTags)
          // don't set default tags, when already provided
          if (!metric.tags['os.host'] && kubernetesTags.os.host) {
            metric.tags['os.host'] = kubernetesTags.os.host
          }
          if (
            !metric.tags['container.id'] &&
            kubernetesTags &&
            kubernetesTags.container &&
            kubernetesTags.container.id
          ) {
            metric.tags['container.id'] = kubernetesTags.container.id
          }
          if (
            !metric.tags['container.name'] &&
            kubernetesTags &&
            kubernetesTags.container &&
            kubernetesTags.container.name
          ) {
            metric.tags['container.name'] = kubernetesTags.container.name
          }
          if (
            !metric.tags['container.image.name'] &&
            kubernetesTags &&
            kubernetesTags.image &&
            kubernetesTags.image.name
          ) {
            metric.tags['container.image.name'] = kubernetesTags.image.name
          }
          if (
            !metric.tags['container.image.tag'] &&
            kubernetesTags &&
            kubernetesTags.image &&
            kubernetesTags.image.tag
          ) {
            metric.tags['container.image.tag'] = kubernetesTags.image.tag
          }
          if (
            !metric.tags['container.image.digest'] &&
            kubernetesTags &&
            kubernetesTags.image &&
            kubernetesTags.image.digest
          ) {
            metric.tags['container.image.digest'] = kubernetesTags.image.digest
          }
          if (!metric.tags['container.pod.name'] && process.env.POD_NAME) {
            metric.tags['container.pod.name'] = process.env.POD_NAME
          }
          if (
            !metric.tags['container.pod.namespace'] &&
            process.env.POD_NAMESPACE
          ) {
            metric.tags['container.pod.namespace'] = process.env.POD_NAMESPACE
          }
        }

        // Set container tags only for container metrics if running in Docker
        const isDkr = process.env.isDocker === 'true'
        if (isDkr) {
          // don't set default tags, when already provided
          if (!metric.tags['container.id'] && process.env.containerId) {
            metric.tags['container.id'] = process.env.containerId
          }
          if (!metric.tags['container.name'] && process.env.containerName) {
            metric.tags['container.name'] = process.env.containerName
          }
          if (
            !metric.tags['container.image.name'] &&
            process.env.containerImageName
          ) {
            metric.tags['container.image.name'] = process.env.containerImageName
          }
          if (
            !metric.tags['container.image.tag'] &&
            process.env.containerImageTag
          ) {
            metric.tags['container.image.tag'] = process.env.containerImageTag
          }
          if (
            !metric.tags['container.image.digest'] &&
            process.env.containerImageDigest
          ) {
            metric.tags['container.image.digest'] =
              process.env.containerImageDigest
          }
          if (!metric.tags['swarm.stack.name'] && process.env.swarmStackName) {
            metric.tags['swarm.stack.name'] = process.env.swarmStackName
          }
          if (
            !metric.tags['swarm.service.name'] &&
            process.env.swarmServiceName
          ) {
            metric.tags['swarm.service.name'] = process.env.swarmServiceName
          }
        }
      }

      // save the metric token tag, in case global tags set 'token'
      const token = metric.tags.token
      // add global tags to metrics Object
      Object.assign(metric.tags, this.globalTags || {})
      // set metric specific token, e.g. in case of infra-app token
      if (token) {
        metric.tags.token = token
      }
    },

    collectdFormatLine: function (metric) {
      var now = new Date().getTime().toFixed(0)
      var metricsTs = metric.ts.toFixed(0)
      var metricsTs2 = (metric.ts / 1000).toFixed(0)
      var dateString = moment(metric.ts).format('YYYY-MM-DD')
      var line = ''
      if (/collectd.*-/.test(metric.name)) {
        if (
          /collectd-cpu/.test(metric.name) ||
          /collectd-io-octets/.test(metric.name)
        ) {
          line =
            now +
            '\t' +
            metric.name +
            '-' +
            dateString +
            ',' +
            metricsTs2 +
            ',' +
            metric.value
        } else if (/disk/.test(metric.name)) {
          line =
            now +
            '\t' +
            metric.name +
            '-' +
            dateString +
            ',' +
            metricsTs2 +
            ',' +
            metric.value.toFixed(6)
        } else {
          line =
            now + '\t' + metric.name + '\t' + metricsTs2 + ',' + metric.value
        }
      } else {
        line = now + '\t' + metric.name + '\t' + metricsTs + ',' + metric.value
      }
      return line
    },

    defaultFormatLine: function (metric) {
      var now = new Date().getTime().toFixed(0)
      var line = null
      if (metric.sct === 'OS') {
        line = this.collectdFormatLine(metric)
      } else {
        line = util.format(
          '%d\t%s\t%d\t%s\t%s',
          now,
          (metric.type || this.defaultType) + '-' + metric.name,
          metric.ts,
          formatArray(metric.filters),
          formatArray(metric.value)
        )
        logger.log(line)
      }
      return line
    },
    /**
     * formats a line for SPM sender, if the plugin does not support this method the default formatter for nodejs and OP metrics is used
     * @maram metric to format
     */
    formatLine: function (metric) {
      if (!this.plugin.formatLine) {
        return this.defaultFormatLine(metric)
      } else {
        return this.plugin.formatLine(metric)
      }
    },
    generateGlobalTags: function (tagNames, tagObject) {
      if (tagNames) {
        if (typeof tagNames === 'string') {
          // convert comma separated String list to array
          tagNames = tagNames.replace(/\s/g, '').split(',')
        }
        if (tagNames instanceof Array) {
          tagNames.forEach(tagName => {
            if (!tagName || tagName === '') {
              return
            }
            if (tagName.indexOf(':') > -1) {
              // static tag a=b
              const keyValue = tagName.split(':')
              if (keyValue.length === 2) {
                tagObject[keyValue[0]] = keyValue[1]
              }
            } else {
              tagObject[tagName] = process.env[tagName]
            }
          })
        }
      }
    },

    /**
     * Starts the agent - typically agents create listeners or interval checks for metrics,
     * and use 'addMetrics' to inform listeners
     */
    start: function () {
      if (!this.globalTags) {
        this.globalTags = {}
      }
      const tagNames =
        process.env.MONITORING_TAGS_FROM_ENV || config.influx.tagsFromEnv
      this.generateGlobalTags(tagNames, this.globalTags)
      if (this.plugin && this.plugin.start) {
        // this is the place to register for external events that return metrics
        this.plugin.start(this)
      } else {
        console.log('NOT called start')
      }
    },
    /**
     * Stops the plugin - timers could be pushed to plugins 'timers' Array, if timers exists it tries to clearInterval(timerId)
     * and tries to call the stop() method of the agent-plugin
     */
    stop: function () {
      if (this.plugin.timers) {
        this.plugin.timers.forEach(function (tid) {
          clearInterval(tid)
        })
      }
      if (this.plugin.stop) {
        this.plugin.stop()
      } else {
        logger.warn('Plugin ' + plugin.name + ' has no stop() function')
      }
    }
  }
  return Object.assign(eventEmitter, agentSuperClass)
}

module.exports = Agent
<|endoftext|>