mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-04 10:41:14 +00:00
In union option groups, the list of selected ids is encoded as a comma-delimited string. For example, if 'foo' and 'bar' are selected, the value 'foo,bar' is sent, ie ["foo", "bar"] -> "foo,bar" Under this scheme, with nothing selected, the empty string should be sent, ie. [] -> "" Before this change, the frontend code called the "none option" by id 'none'. There were several issues with this: * The frontend would send 'none' when nothing was selected, not ''. The backend ignored this as it ignores junk values in the options, treating them as though they hadn't been given. * The backend would attempt to set the default value of an option to "nothing selected", ie. [], by sending ''. The frontend would interpret this as nothing selected, *not even the 'none' option*, which caused a visual bug. * Everything would break if one of the legitimate options had the id 'none', which could easily happen eg. if a user had a 'none' k8s namespace. This is perhaps an unusual name, but our code shouldn't break when a particular arbitary string is used as an input. With this change, the none option is called '', which fixes all the above problems: * The frontend encodes [''] as '' * The frontend decodes '' as [''], and therefore shows the '' option as selected * The string '' is not a valid k8s namespace name and is a reasonable "prohibited value" for all other use cases. The backend already couldn't handle a value with this id correctly prior to this change anyway.
135 lines
4.7 KiB
JavaScript
135 lines
4.7 KiB
JavaScript
import React from 'react';
|
|
import { connect } from 'react-redux';
|
|
import { Map as makeMap } from 'immutable';
|
|
import includes from 'lodash/includes';
|
|
|
|
import { trackMixpanelEvent } 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) {
|
|
trackMixpanelEvent('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');
|
|
const activeValue = activeOptions && activeOptions.has(optionId)
|
|
? activeOptions.get(optionId)
|
|
: option.get('defaultValue');
|
|
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);
|