<commit_before>import React from 'react'
import PropTypes from 'prop-types'
import { parse as parseRemote } from 'xo-remote-parser'
import {
  assign,
  filter,
  flatten,
  forEach,
  get,
  groupBy,
  includes,
  isArray,
  isEmpty,
  isInteger,
  isString,
  keyBy,
  keys,
  map,
  mapValues,
  pick,
  sortBy,
  toArray,
} from 'lodash'

import _ from './intl'
import Button from './button'
import Icon from './icon'
import renderXoItem from './render-xo-item'
import Select from './form/select'
import store from './store'
import Tooltip from './tooltip'
import uncontrollableInput from 'uncontrollable-input'
import {
  createCollectionWrapper,
  createFilter,
  createGetObjectsOfType,
  createGetTags,
  createSelector,
  getObject,
} from './selectors'
import { addSubscriptions, connectStore, resolveResourceSets } from './utils'
import {
  isSrWritable,
  subscribeCurrentUser,
  subscribeGroups,
  subscribeIpPools,
  subscribeRemotes,
  subscribeResourceSets,
  subscribeRoles,
  subscribeUsers,
} from './xo'

// ===================================================================

// react-select's line-height is 1.4
// https://github.com/JedWatson/react-select/blob/916ab0e62fc7394be8e24f22251c399a68de8b1c/less/multi.less#L33
// while bootstrap button's line-height is 1.25
// https://github.com/twbs/bootstrap/blob/959c4e527c6ef69623928db638267ba1c370479d/scss/_variables.scss#L342
const ADDON_BUTTON_STYLE = { lineHeight: '1.4' }

const getIds = value =>
  value == null || isString(value) || isInteger(value)
    ? value
    : isArray(value) ? map(value, getIds) : value.id

const getOption = (object, container) => ({
  label: container
    ? `${getLabel(object)} ${getLabel(container)}`
    : getLabel(object),
  value: object.id,
  xoItem: object,
})

const getLabel = object =>
  object.name_label ||
  object.name ||
  object.email ||
  (object.value && object.value.name) ||
  object.value ||
  object.label

const options = props => ({
  defaultValue: props.multi ? [] : undefined,
})

// ===================================================================

/*
 * WITHOUT xoContainers :
 *
 * xoObjects: [
 *  { type: 'myType', id: 'abc', label: 'First object' },
 *  { type: 'myType', id: 'def', label: 'Second object' }
 * ]
 *
 *
 * WITH xoContainers :
 *
 * xoContainers: [
 *  { type: 'containerType', id: 'ghi', label: 'First container' },
 *  { type: 'containerType', id: 'jkl', label: 'Second container' }
 * ]
 *
 * xoObjects: {
 *  ghi: [
 *    { type: 'objectType', id: 'mno', label: 'First object' }
 *    { type: 'objectType', id: 'pqr', label: 'Second object' }
 *  ],
 *  jkl: [
 *    { type: 'objectType', id: 'stu', label: 'Third object' }
 *  ]
 * }
 */
class GenericSelect extends React.Component {
  static propTypes = {
    hasSelectAll: PropTypes.bool,
    multi: PropTypes.bool,
    onChange: PropTypes.func.isRequired,
    xoContainers: PropTypes.array,
    xoObjects: PropTypes.oneOfType([
      PropTypes.array,
      PropTypes.objectOf(PropTypes.array),
    ]).isRequired,
  }

  _getObjectsById = createSelector(
    () => this.props.xoObjects,
    objects =>
      keyBy(isArray(objects) ? objects : flatten(toArray(objects)), 'id')
  )

  _getOptions = createSelector(
    () => this.props.xoContainers,
    () => this.props.xoObjects,
    (containers, objects) => {
      // createCollectionWrapper with a depth?
      const { name } = this.constructor

      let options
      if (containers === undefined) {
        if (__DEV__ && !isArray(objects)) {
          throw new Error(
            `${name}: without xoContainers, xoObjects must be an array`
          )
        }

        options = map(objects, getOption)
      } else {
        if (__DEV__ && isArray(objects)) {
          throw new Error(
            `${name}: with xoContainers, xoObjects must be an object`
          )
        }

        options = []
        forEach(containers, container => {
          options.push({
            disabled: true,
            xoItem: container,
          })

          forEach(objects[container.id], object => {
            options.push(getOption(object, container))
          })
        })
      }

      const objectsById = this._getObjectsById()
      const addIfMissing = val => {
        if (val != null && !(val in objectsById)) {
          options.push({
            disabled: true,
            id: val,
            label: val,
            value: val,
            xoItem: {
              id: val,
              removed: true,
            },
          })
        }
      }

      const values = this._getSelectedIds()
      if (isArray(values)) {
        forEach(values, addIfMissing)
      } else {
        addIfMissing(values)
      }

      return options
    }
  )

  _getSelectedIds = createSelector(
    () => this.props.value,
    createCollectionWrapper(getIds)
  )

  _getSelectedObjects = (() => {
    const helper = createSelector(
      this._getObjectsById,
      value => value,
      (objectsById, value) =>
        isArray(value)
          ? map(value, value => objectsById[value.value])
          : objectsById[value.value]
    )
    return value => (value == null ? value : helper(value))
  })()

  _onChange = value => {
    this.props.onChange(this._getSelectedObjects(value))
  }

  _selectAll = () => {
    this._onChange(filter(this._getOptions(), ({ disabled }) => !disabled))
  }

  // GroupBy: Display option with margin if not disabled and containers exists.
  _renderOption = option => (
    <span
      className={
        !option.disabled && this.props.xoContainers !== undefined
          ? 'ml-1'
          : undefined
      }
    >
      {renderXoItem(option.xoItem)}
    </span>
  )

  render () {
    const { hasSelectAll, xoContainers, xoObjects, ...props } = this.props

    const select = (
      <Select
        {...props}
        onChange={this._onChange}
        openOnFocus
        optionRenderer={this._renderOption}
        options={this._getOptions()}
        value={this._getSelectedIds()}
      />
    )

    if (!props.multi || !hasSelectAll) {
      return select
    }

    // `hasSelectAll` should be provided by react-select after this pull request has been merged:
    // https://github.com/JedWatson/react-select/pull/748
    // TODO: remove once it has been merged upstream.
    return (
      <div className='input-group'>
        {select}
        <span className='input-group-btn'>
          <Tooltip content={_('selectAll')}>
            <Button onClick={this._selectAll} style={ADDON_BUTTON_STYLE}>
              <Icon icon='add' />
            </Button>
          </Tooltip>
        </span>
      </div>
    )
  }
}

const makeStoreSelect = (createSelectors, defaultProps) =>
  uncontrollableInput(options)(
    connectStore(createSelectors)(props => (
      <GenericSelect {...defaultProps} {...props} />
    ))
  )

const makeSubscriptionSelect = (subscribe, props) =>
  uncontrollableInput(options)(
    class extends React.PureComponent {
      state = {}

      _getFilteredXoContainers = createFilter(
        () => this.state.xoContainers,
        () => this.props.containerPredicate
      )

      _getFilteredXoObjects = createSelector(
        () => this.state.xoObjects,
        () => this.state.xoContainers && this._getFilteredXoContainers(),
        () => this.props.predicate,
        (xoObjects, xoContainers, predicate) => {
          if (xoContainers == null) {
            return filter(xoObjects, predicate)
          } else {
            // Filter xoObjects with `predicate`...
            const filteredObjects = mapValues(xoObjects, xoObjectsGroup =>
              filter(xoObjectsGroup, predicate)
            )
            // ...and keep only those whose xoContainer hasn't been filtered out
            return pick(
              filteredObjects,
              map(xoContainers, container => container.id)
            )
          }
        }
      )

      componentWillMount () {
        this.componentWillUnmount = subscribe(::this.setState)
      }

      render () {
        return (
          <GenericSelect
            {...props}
            {...this.props}
            xoObjects={this._getFilteredXoObjects()}
            xoContainers={
              this.state.xoContainers && this._getFilteredXoContainers()
            }
          />
        )
      }
    }
  )

// ===================================================================
// XO objects.
// ===================================================================

const getPredicate = (state, props) => props.predicate

// ===================================================================

export const SelectHost = makeStoreSelect(
  () => {
    const getHostsByPool = createGetObjectsOfType('host')
      .filter(getPredicate)
      .sort()

    return {
      xoObjects: getHostsByPool,
    }
  },
  { placeholder: _('selectHosts') }
)

// ===================================================================

export const SelectPool = makeStoreSelect(
  () => ({
    xoObjects: createGetObjectsOfType('pool')
      .filter(getPredicate)
      .sort(),
  }),
  { placeholder: _('selectPools') }
)

// ===================================================================

export const SelectSr = makeStoreSelect(
  () => {
    const getPools = createGetObjectsOfType('pools')
    const getHosts = createGetObjectsOfType('host')

    const getSrsByContainer = createSelector(
      createGetObjectsOfType('SR')
        .filter((_, { predicate }) => predicate || isSrWritable)
        .sort(),<commit_msg>fix(xo-web/SelectSr): "pool" typo (#2878)<commit_after>import React from 'react'
import PropTypes from 'prop-types'
import { parse as parseRemote } from 'xo-remote-parser'
import {
  assign,
  filter,
  flatten,
  forEach,
  get,
  groupBy,
  includes,
  isArray,
  isEmpty,
  isInteger,
  isString,
  keyBy,
  keys,
  map,
  mapValues,
  pick,
  sortBy,
  toArray,
} from 'lodash'

import _ from './intl'
import Button from './button'
import Icon from './icon'
import renderXoItem from './render-xo-item'
import Select from './form/select'
import store from './store'
import Tooltip from './tooltip'
import uncontrollableInput from 'uncontrollable-input'
import {
  createCollectionWrapper,
  createFilter,
  createGetObjectsOfType,
  createGetTags,
  createSelector,
  getObject,
} from './selectors'
import { addSubscriptions, connectStore, resolveResourceSets } from './utils'
import {
  isSrWritable,
  subscribeCurrentUser,
  subscribeGroups,
  subscribeIpPools,
  subscribeRemotes,
  subscribeResourceSets,
  subscribeRoles,
  subscribeUsers,
} from './xo'

// ===================================================================

// react-select's line-height is 1.4
// https://github.com/JedWatson/react-select/blob/916ab0e62fc7394be8e24f22251c399a68de8b1c/less/multi.less#L33
// while bootstrap button's line-height is 1.25
// https://github.com/twbs/bootstrap/blob/959c4e527c6ef69623928db638267ba1c370479d/scss/_variables.scss#L342
const ADDON_BUTTON_STYLE = { lineHeight: '1.4' }

const getIds = value =>
  value == null || isString(value) || isInteger(value)
    ? value
    : isArray(value) ? map(value, getIds) : value.id

const getOption = (object, container) => ({
  label: container
    ? `${getLabel(object)} ${getLabel(container)}`
    : getLabel(object),
  value: object.id,
  xoItem: object,
})

const getLabel = object =>
  object.name_label ||
  object.name ||
  object.email ||
  (object.value && object.value.name) ||
  object.value ||
  object.label

const options = props => ({
  defaultValue: props.multi ? [] : undefined,
})

// ===================================================================

/*
 * WITHOUT xoContainers :
 *
 * xoObjects: [
 *  { type: 'myType', id: 'abc', label: 'First object' },
 *  { type: 'myType', id: 'def', label: 'Second object' }
 * ]
 *
 *
 * WITH xoContainers :
 *
 * xoContainers: [
 *  { type: 'containerType', id: 'ghi', label: 'First container' },
 *  { type: 'containerType', id: 'jkl', label: 'Second container' }
 * ]
 *
 * xoObjects: {
 *  ghi: [
 *    { type: 'objectType', id: 'mno', label: 'First object' }
 *    { type: 'objectType', id: 'pqr', label: 'Second object' }
 *  ],
 *  jkl: [
 *    { type: 'objectType', id: 'stu', label: 'Third object' }
 *  ]
 * }
 */
class GenericSelect extends React.Component {
  static propTypes = {
    hasSelectAll: PropTypes.bool,
    multi: PropTypes.bool,
    onChange: PropTypes.func.isRequired,
    xoContainers: PropTypes.array,
    xoObjects: PropTypes.oneOfType([
      PropTypes.array,
      PropTypes.objectOf(PropTypes.array),
    ]).isRequired,
  }

  _getObjectsById = createSelector(
    () => this.props.xoObjects,
    objects =>
      keyBy(isArray(objects) ? objects : flatten(toArray(objects)), 'id')
  )

  _getOptions = createSelector(
    () => this.props.xoContainers,
    () => this.props.xoObjects,
    (containers, objects) => {
      // createCollectionWrapper with a depth?
      const { name } = this.constructor

      let options
      if (containers === undefined) {
        if (__DEV__ && !isArray(objects)) {
          throw new Error(
            `${name}: without xoContainers, xoObjects must be an array`
          )
        }

        options = map(objects, getOption)
      } else {
        if (__DEV__ && isArray(objects)) {
          throw new Error(
            `${name}: with xoContainers, xoObjects must be an object`
          )
        }

        options = []
        forEach(containers, container => {
          options.push({
            disabled: true,
            xoItem: container,
          })

          forEach(objects[container.id], object => {
            options.push(getOption(object, container))
          })
        })
      }

      const objectsById = this._getObjectsById()
      const addIfMissing = val => {
        if (val != null && !(val in objectsById)) {
          options.push({
            disabled: true,
            id: val,
            label: val,
            value: val,
            xoItem: {
              id: val,
              removed: true,
            },
          })
        }
      }

      const values = this._getSelectedIds()
      if (isArray(values)) {
        forEach(values, addIfMissing)
      } else {
        addIfMissing(values)
      }

      return options
    }
  )

  _getSelectedIds = createSelector(
    () => this.props.value,
    createCollectionWrapper(getIds)
  )

  _getSelectedObjects = (() => {
    const helper = createSelector(
      this._getObjectsById,
      value => value,
      (objectsById, value) =>
        isArray(value)
          ? map(value, value => objectsById[value.value])
          : objectsById[value.value]
    )
    return value => (value == null ? value : helper(value))
  })()

  _onChange = value => {
    this.props.onChange(this._getSelectedObjects(value))
  }

  _selectAll = () => {
    this._onChange(filter(this._getOptions(), ({ disabled }) => !disabled))
  }

  // GroupBy: Display option with margin if not disabled and containers exists.
  _renderOption = option => (
    <span
      className={
        !option.disabled && this.props.xoContainers !== undefined
          ? 'ml-1'
          : undefined
      }
    >
      {renderXoItem(option.xoItem)}
    </span>
  )

  render () {
    const { hasSelectAll, xoContainers, xoObjects, ...props } = this.props

    const select = (
      <Select
        {...props}
        onChange={this._onChange}
        openOnFocus
        optionRenderer={this._renderOption}
        options={this._getOptions()}
        value={this._getSelectedIds()}
      />
    )

    if (!props.multi || !hasSelectAll) {
      return select
    }

    // `hasSelectAll` should be provided by react-select after this pull request has been merged:
    // https://github.com/JedWatson/react-select/pull/748
    // TODO: remove once it has been merged upstream.
    return (
      <div className='input-group'>
        {select}
        <span className='input-group-btn'>
          <Tooltip content={_('selectAll')}>
            <Button onClick={this._selectAll} style={ADDON_BUTTON_STYLE}>
              <Icon icon='add' />
            </Button>
          </Tooltip>
        </span>
      </div>
    )
  }
}

const makeStoreSelect = (createSelectors, defaultProps) =>
  uncontrollableInput(options)(
    connectStore(createSelectors)(props => (
      <GenericSelect {...defaultProps} {...props} />
    ))
  )

const makeSubscriptionSelect = (subscribe, props) =>
  uncontrollableInput(options)(
    class extends React.PureComponent {
      state = {}

      _getFilteredXoContainers = createFilter(
        () => this.state.xoContainers,
        () => this.props.containerPredicate
      )

      _getFilteredXoObjects = createSelector(
        () => this.state.xoObjects,
        () => this.state.xoContainers && this._getFilteredXoContainers(),
        () => this.props.predicate,
        (xoObjects, xoContainers, predicate) => {
          if (xoContainers == null) {
            return filter(xoObjects, predicate)
          } else {
            // Filter xoObjects with `predicate`...
            const filteredObjects = mapValues(xoObjects, xoObjectsGroup =>
              filter(xoObjectsGroup, predicate)
            )
            // ...and keep only those whose xoContainer hasn't been filtered out
            return pick(
              filteredObjects,
              map(xoContainers, container => container.id)
            )
          }
        }
      )

      componentWillMount () {
        this.componentWillUnmount = subscribe(::this.setState)
      }

      render () {
        return (
          <GenericSelect
            {...props}
            {...this.props}
            xoObjects={this._getFilteredXoObjects()}
            xoContainers={
              this.state.xoContainers && this._getFilteredXoContainers()
            }
          />
        )
      }
    }
  )

// ===================================================================
// XO objects.
// ===================================================================

const getPredicate = (state, props) => props.predicate

// ===================================================================

export const SelectHost = makeStoreSelect(
  () => {
    const getHostsByPool = createGetObjectsOfType('host')
      .filter(getPredicate)
      .sort()

    return {
      xoObjects: getHostsByPool,
    }
  },
  { placeholder: _('selectHosts') }
)

// ===================================================================

export const SelectPool = makeStoreSelect(
  () => ({
    xoObjects: createGetObjectsOfType('pool')
      .filter(getPredicate)
      .sort(),
  }),
  { placeholder: _('selectPools') }
)

// ===================================================================

export const SelectSr = makeStoreSelect(
  () => {
    const getPools = createGetObjectsOfType('pool')
    const getHosts = createGetObjectsOfType('host')

    const getSrsByContainer = createSelector(
      createGetObjectsOfType('SR')
        .filter((_, { predicate }) => predicate || isSrWritable)
        .sort(),
{'fraction_non_alphanumeric': 0.17459235568604833, 'fraction_numerical': 0.007549939705342631, 'mean_word_length': 2.5309144761199556, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 107, 'https://': 6, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1359762', 'n_tokens_mistral': 5792, 'n_tokens_neox': 5594, 'n_words': 1146}