import React from 'react'; import { connect } from 'react-redux'; import { Set as makeSet, Map as makeMap } from 'immutable'; import includes from 'lodash/includes'; import { trackAnalyticsEvent } from '../utils/tracking-utils'; import { getCurrentTopologyOptions } from '../utils/topology-utils'; import { activeTopologyOptionsSelector } from '../selectors/topology'; import TopologyOptionAction from './topology-option-action'; import { changeTopologyOption } from '../actions/request-actions'; class TopologyOptions extends React.Component { constructor(props, context) { super(props, context); this.trackOptionClick = this.trackOptionClick.bind(this); this.handleOptionClick = this.handleOptionClick.bind(this); this.handleNoneClick = this.handleNoneClick.bind(this); } trackOptionClick(optionId, nextOptions) { trackAnalyticsEvent('scope.topology.option.click', { layout: this.props.topologyViewMode, optionId, parentTopologyId: this.props.currentTopology.get('parentId'), topologyId: this.props.currentTopology.get('id'), value: nextOptions, }); } handleOptionClick(optionId, value, topologyId) { let nextOptions = [value]; const { activeOptions, options } = this.props; const selectedOption = options.find(o => o.get('id') === optionId); if (selectedOption.get('selectType') === 'union') { // Multi-select topology options (such as k8s namespaces) are handled here. // Users can select one, many, or none of these options. // The component builds an array of the next selected values that are sent to the action. const opts = activeOptions.toJS(); const selected = selectedOption.get('id'); const selectedActiveOptions = opts[selected] || []; const isSelectedAlready = includes(selectedActiveOptions, value); if (isSelectedAlready) { // Remove the option if it is already selected nextOptions = selectedActiveOptions.filter(o => o !== value); } else { // Add it to the array if it's not selected nextOptions = selectedActiveOptions.concat(value); } // Since the user is clicking an option, remove the highlighting from the none option, // unless they are removing the last option. In that case, default to the none label. // Note that since the other ids are potentially user-controlled (eg. k8s namespaces), // the only string we can use for the none option is the empty string '', // since that can't collide. if (nextOptions.length === 0) { nextOptions = ['']; } else { nextOptions = nextOptions.filter(o => o !== ''); } } this.trackOptionClick(optionId, nextOptions); this.props.changeTopologyOption(optionId, nextOptions, topologyId); } handleNoneClick(optionId, value, topologyId) { const nextOptions = ['']; this.trackOptionClick(optionId, nextOptions); this.props.changeTopologyOption(optionId, nextOptions, topologyId); } renderOption(option) { const { activeOptions, currentTopologyId } = this.props; const optionId = option.get('id'); // Make the active value be the intersection of the available options // and the active selection and use the default value if there is no // overlap. It seems intuitive that active selection would always be a // subset of available option, but the exception can happen when going // back in time (making available options change, while not touching // the selection). // TODO: This logic should probably be made consistent with how topology // selection is handled when time travelling, especially when the name- // spaces are brought under category selection. // TODO: Consider extracting this into a global selector. let activeValue = option.get('defaultValue'); if (activeOptions && activeOptions.has(optionId)) { const activeSelection = makeSet(activeOptions.get(optionId)); const availableOptions = makeSet(option.get('options').map(o => o.get('value'))); const intersection = activeSelection.intersect(availableOptions); if (!intersection.isEmpty()) { activeValue = intersection.toJS(); } } const noneItem = makeMap({ label: option.get('noneLabel'), value: '' }); return (