topology-options.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import React from 'react';
  2. import { connect } from 'react-redux';
  3. import { Set as makeSet, Map as makeMap } from 'immutable';
  4. import includes from 'lodash/includes';
  5. import { trackAnalyticsEvent } from '../utils/tracking-utils';
  6. import { getCurrentTopologyOptions } from '../utils/topology-utils';
  7. import { activeTopologyOptionsSelector } from '../selectors/topology';
  8. import TopologyOptionAction from './topology-option-action';
  9. import { changeTopologyOption } from '../actions/request-actions';
  10. class TopologyOptions extends React.Component {
  11. constructor(props, context) {
  12. super(props, context);
  13. this.trackOptionClick = this.trackOptionClick.bind(this);
  14. this.handleOptionClick = this.handleOptionClick.bind(this);
  15. this.handleNoneClick = this.handleNoneClick.bind(this);
  16. }
  17. trackOptionClick(optionId, nextOptions) {
  18. trackAnalyticsEvent('scope.topology.option.click', {
  19. layout: this.props.topologyViewMode,
  20. optionId,
  21. parentTopologyId: this.props.currentTopology.get('parentId'),
  22. topologyId: this.props.currentTopology.get('id'),
  23. value: nextOptions,
  24. });
  25. }
  26. handleOptionClick(optionId, value, topologyId) {
  27. let nextOptions = [value];
  28. const { activeOptions, options } = this.props;
  29. const selectedOption = options.find(o => o.get('id') === optionId);
  30. if (selectedOption.get('selectType') === 'union') {
  31. // Multi-select topology options (such as k8s namespaces) are handled here.
  32. // Users can select one, many, or none of these options.
  33. // The component builds an array of the next selected values that are sent to the action.
  34. const opts = activeOptions.toJS();
  35. const selected = selectedOption.get('id');
  36. const selectedActiveOptions = opts[selected] || [];
  37. const isSelectedAlready = includes(selectedActiveOptions, value);
  38. if (isSelectedAlready) {
  39. // Remove the option if it is already selected
  40. nextOptions = selectedActiveOptions.filter(o => o !== value);
  41. } else {
  42. // Add it to the array if it's not selected
  43. nextOptions = selectedActiveOptions.concat(value);
  44. }
  45. // Since the user is clicking an option, remove the highlighting from the none option,
  46. // unless they are removing the last option. In that case, default to the none label.
  47. // Note that since the other ids are potentially user-controlled (eg. k8s namespaces),
  48. // the only string we can use for the none option is the empty string '',
  49. // since that can't collide.
  50. if (nextOptions.length === 0) {
  51. nextOptions = [''];
  52. } else {
  53. nextOptions = nextOptions.filter(o => o !== '');
  54. }
  55. }
  56. this.trackOptionClick(optionId, nextOptions);
  57. this.props.changeTopologyOption(optionId, nextOptions, topologyId);
  58. }
  59. handleNoneClick(optionId, value, topologyId) {
  60. const nextOptions = [''];
  61. this.trackOptionClick(optionId, nextOptions);
  62. this.props.changeTopologyOption(optionId, nextOptions, topologyId);
  63. }
  64. renderOption(option) {
  65. const { activeOptions, currentTopologyId } = this.props;
  66. const optionId = option.get('id');
  67. // Make the active value be the intersection of the available options
  68. // and the active selection and use the default value if there is no
  69. // overlap. It seems intuitive that active selection would always be a
  70. // subset of available option, but the exception can happen when going
  71. // back in time (making available options change, while not touching
  72. // the selection).
  73. // TODO: This logic should probably be made consistent with how topology
  74. // selection is handled when time travelling, especially when the name-
  75. // spaces are brought under category selection.
  76. // TODO: Consider extracting this into a global selector.
  77. let activeValue = option.get('defaultValue');
  78. if (activeOptions && activeOptions.has(optionId)) {
  79. const activeSelection = makeSet(activeOptions.get(optionId));
  80. const availableOptions = makeSet(option.get('options').map(o => o.get('value')));
  81. const intersection = activeSelection.intersect(availableOptions);
  82. if (!intersection.isEmpty()) {
  83. activeValue = intersection.toJS();
  84. }
  85. }
  86. const noneItem = makeMap({
  87. label: option.get('noneLabel'),
  88. value: ''
  89. });
  90. return (
  91. <div className="topology-option" key={optionId}>
  92. <div className="topology-option-wrapper">
  93. {option.get('selectType') === 'union'
  94. && (
  95. <TopologyOptionAction
  96. onClick={this.handleNoneClick}
  97. optionId={optionId}
  98. item={noneItem}
  99. topologyId={currentTopologyId}
  100. activeValue={activeValue}
  101. />
  102. )
  103. }
  104. {option.get('options').map(item => (
  105. <TopologyOptionAction
  106. onClick={this.handleOptionClick}
  107. optionId={optionId}
  108. topologyId={currentTopologyId}
  109. key={item.get('value')}
  110. activeValue={activeValue}
  111. item={item}
  112. />
  113. ))}
  114. </div>
  115. </div>
  116. );
  117. }
  118. render() {
  119. const { options } = this.props;
  120. return (
  121. <div className="topology-options">
  122. {options && options.toIndexedSeq().map(option => this.renderOption(option))}
  123. </div>
  124. );
  125. }
  126. }
  127. function mapStateToProps(state) {
  128. return {
  129. activeOptions: activeTopologyOptionsSelector(state),
  130. currentTopology: state.get('currentTopology'),
  131. currentTopologyId: state.get('currentTopologyId'),
  132. options: getCurrentTopologyOptions(state),
  133. topologyViewMode: state.get('topologyViewMode')
  134. };
  135. }
  136. export default connect(
  137. mapStateToProps,
  138. { changeTopologyOption }
  139. )(TopologyOptions);