mirror of
https://github.com/weaveworks/scope.git
synced 2026-04-20 01:17:09 +00:00
152 lines
5.6 KiB
JavaScript
152 lines
5.6 KiB
JavaScript
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/app-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', {
|
|
optionId,
|
|
value: nextOptions,
|
|
layout: this.props.topologyViewMode,
|
|
topologyId: this.props.currentTopology.get('id'),
|
|
parentTopologyId: this.props.currentTopology.get('parentId'),
|
|
});
|
|
}
|
|
|
|
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({
|
|
value: '',
|
|
label: option.get('noneLabel')
|
|
});
|
|
return (
|
|
<div className="topology-option" key={optionId}>
|
|
<div className="topology-option-wrapper">
|
|
{option.get('options').map(item => (
|
|
<TopologyOptionAction
|
|
onClick={this.handleOptionClick}
|
|
optionId={optionId}
|
|
topologyId={currentTopologyId}
|
|
key={item.get('value')}
|
|
activeValue={activeValue}
|
|
item={item}
|
|
/>
|
|
))}
|
|
{option.get('selectType') === 'union' &&
|
|
<TopologyOptionAction
|
|
onClick={this.handleNoneClick}
|
|
optionId={optionId}
|
|
item={noneItem}
|
|
topologyId={currentTopologyId}
|
|
activeValue={activeValue}
|
|
/>
|
|
}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const { options } = this.props;
|
|
return (
|
|
<div className="topology-options">
|
|
{options && options.toIndexedSeq().map(option => this.renderOption(option))}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
function mapStateToProps(state) {
|
|
return {
|
|
options: getCurrentTopologyOptions(state),
|
|
topologyViewMode: state.get('topologyViewMode'),
|
|
currentTopology: state.get('currentTopology'),
|
|
currentTopologyId: state.get('currentTopologyId'),
|
|
activeOptions: activeTopologyOptionsSelector(state)
|
|
};
|
|
}
|
|
|
|
export default connect(
|
|
mapStateToProps,
|
|
{ changeTopologyOption }
|
|
)(TopologyOptions);
|