/* eslint-disable react/no-multi-comp */ import { isString, extend, each, has, map, includes, findIndex, find, fromPairs, clone, isEmpty } from "lodash"; import React, { Fragment } from "react"; import PropTypes from "prop-types"; import classNames from "classnames"; import Select from "antd/lib/select"; import Table from "antd/lib/table"; import Popover from "antd/lib/popover"; import Button from "antd/lib/button"; import Icon from "antd/lib/icon"; import Tag from "antd/lib/tag"; import Input from "antd/lib/input"; import Radio from "antd/lib/radio"; import Form from "antd/lib/form"; import Tooltip from "antd/lib/tooltip"; import ParameterValueInput from "@/components/ParameterValueInput"; import { ParameterMappingType } from "@/services/widget"; import { Parameter } from "@/services/parameters"; import HelpTrigger from "@/components/HelpTrigger"; import "./ParameterMappingInput.less"; const { Option } = Select; export const MappingType = { DashboardAddNew: "dashboard-add-new", DashboardMapToExisting: "dashboard-map-to-existing", WidgetLevel: "widget-level", StaticValue: "static-value", }; export function parameterMappingsToEditableMappings(mappings, parameters, existingParameterNames = []) { return map(mappings, mapping => { const result = extend({}, mapping); const alreadyExists = includes(existingParameterNames, mapping.mapTo); result.param = find(parameters, p => p.name === mapping.name); switch (mapping.type) { case ParameterMappingType.DashboardLevel: result.type = alreadyExists ? MappingType.DashboardMapToExisting : MappingType.DashboardAddNew; result.value = null; break; case ParameterMappingType.StaticValue: result.type = MappingType.StaticValue; result.param = result.param.clone(); result.param.setValue(result.value); break; case ParameterMappingType.WidgetLevel: result.type = MappingType.WidgetLevel; result.value = null; break; // no default } return result; }); } export function editableMappingsToParameterMappings(mappings) { return fromPairs( map( // convert to map mappings, mapping => { const result = extend({}, mapping); switch (mapping.type) { case MappingType.DashboardAddNew: result.type = ParameterMappingType.DashboardLevel; result.value = null; break; case MappingType.DashboardMapToExisting: result.type = ParameterMappingType.DashboardLevel; result.value = null; break; case MappingType.StaticValue: result.type = ParameterMappingType.StaticValue; result.param = mapping.param.clone(); result.param.setValue(result.value); result.value = result.param.value; break; case MappingType.WidgetLevel: result.type = ParameterMappingType.WidgetLevel; result.value = null; break; // no default } delete result.param; return [result.name, result]; } ) ); } export function synchronizeWidgetTitles(sourceMappings, widgets) { const affectedWidgets = []; each(sourceMappings, sourceMapping => { if (sourceMapping.type === ParameterMappingType.DashboardLevel) { each(widgets, widget => { const widgetMappings = widget.options.parameterMappings; each(widgetMappings, widgetMapping => { // check if mapped to the same dashboard-level parameter if ( widgetMapping.type === ParameterMappingType.DashboardLevel && widgetMapping.mapTo === sourceMapping.mapTo ) { // dirty check - update only when needed if (widgetMapping.title !== sourceMapping.title) { widgetMapping.title = sourceMapping.title; affectedWidgets.push(widget); } } }); }); } }); return affectedWidgets; } export class ParameterMappingInput extends React.Component { static propTypes = { mapping: PropTypes.object, // eslint-disable-line react/forbid-prop-types existingParamNames: PropTypes.arrayOf(PropTypes.string), onChange: PropTypes.func, inputError: PropTypes.string, }; static defaultProps = { mapping: {}, existingParamNames: [], onChange: () => {}, inputError: null, }; formItemProps = { labelCol: { span: 5 }, wrapperCol: { span: 16 }, className: "form-item", }; updateSourceType = type => { let { mapping: { mapTo }, } = this.props; const { existingParamNames } = this.props; // if mapped name doesn't already exists // default to first select option if (type === MappingType.DashboardMapToExisting && !includes(existingParamNames, mapTo)) { mapTo = existingParamNames[0]; } this.updateParamMapping({ type, mapTo }); }; updateParamMapping = update => { const { onChange, mapping } = this.props; const newMapping = extend({}, mapping, update); if (newMapping.value !== mapping.value) { newMapping.param = newMapping.param.clone(); newMapping.param.setValue(newMapping.value); } if (has(update, "type")) { if (update.type === MappingType.StaticValue) { newMapping.value = newMapping.param.value; } else { newMapping.value = null; } } onChange(newMapping); }; renderMappingTypeSelector() { const noExisting = isEmpty(this.props.existingParamNames); return ( this.updateSourceType(e.target.value)}> New dashboard parameter Existing dashboard parameter{" "} {noExisting ? ( ) : null} Widget parameter Static value ); } renderDashboardAddNew() { const { mapping: { mapTo }, } = this.props; return this.updateParamMapping({ mapTo: e.target.value })} />; } renderDashboardMapToExisting() { const { mapping, existingParamNames } = this.props; return ( ); } renderStaticValue() { const { mapping } = this.props; return ( this.updateParamMapping({ value })} /> ); } renderInputBlock() { const { mapping } = this.props; switch (mapping.type) { case MappingType.DashboardAddNew: return ["Key", "Enter a new parameter keyword", this.renderDashboardAddNew()]; case MappingType.DashboardMapToExisting: return ["Key", "Select from a list of existing parameters", this.renderDashboardMapToExisting()]; case MappingType.StaticValue: return ["Value", null, this.renderStaticValue()]; default: return []; } } render() { const { inputError } = this.props; const [label, help, input] = this.renderInputBlock(); return (
{this.renderMappingTypeSelector()} {input}
); } } class MappingEditor extends React.Component { static propTypes = { mapping: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types existingParamNames: PropTypes.arrayOf(PropTypes.string).isRequired, onChange: PropTypes.func.isRequired, }; constructor(props) { super(props); this.state = { visible: false, mapping: clone(this.props.mapping), inputError: null, }; } onVisibleChange = visible => { if (visible) this.show(); else this.hide(); }; onChange = mapping => { let inputError = null; if (mapping.type === MappingType.DashboardAddNew) { if (isEmpty(mapping.mapTo)) { inputError = "Keyword must have a value"; } else if (includes(this.props.existingParamNames, mapping.mapTo)) { inputError = "A parameter with this name already exists"; } } this.setState({ mapping, inputError }); }; save = () => { this.props.onChange(this.props.mapping, this.state.mapping); this.hide(); }; show = () => { this.setState({ visible: true, mapping: clone(this.props.mapping), // restore original state }); }; hide = () => { this.setState({ visible: false }); }; renderContent() { const { mapping, inputError } = this.state; return (
Edit Source and Value
); } render() { const { visible, mapping } = this.state; return ( ); } } class TitleEditor extends React.Component { static propTypes = { existingParams: PropTypes.arrayOf(PropTypes.object), mapping: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types onChange: PropTypes.func.isRequired, }; static defaultProps = { existingParams: [], }; state = { showPopup: false, title: "", // will be set on editing }; onPopupVisibleChange = showPopup => { this.setState({ showPopup, title: showPopup ? this.getMappingTitle() : "", }); }; onEditingTitleChange = event => { this.setState({ title: event.target.value }); }; getMappingTitle() { let { mapping } = this.props; if (isString(mapping.title) && mapping.title !== "") { return mapping.title; } // if mapped to dashboard, find source param and return it's title if (mapping.type === MappingType.DashboardMapToExisting) { const source = find(this.props.existingParams, { name: mapping.mapTo }); if (source) { mapping = source; } } return mapping.title || mapping.param.title; } save = () => { const newMapping = extend({}, this.props.mapping, { title: this.state.title }); this.props.onChange(newMapping); this.hide(); }; hide = () => { this.setState({ showPopup: false }); }; renderPopover() { const { param: { title: paramTitle }, } = this.props.mapping; return (
); } renderEditButton() { const { mapping } = this.props; if (mapping.type === MappingType.StaticValue) { return ( ); } return ( ); } render() { const { mapping } = this.props; // static value are non-editable hence disabled const disabled = mapping.type === MappingType.StaticValue; return (
{this.getMappingTitle()} {this.renderEditButton()}
); } } export class ParameterMappingListInput extends React.Component { static propTypes = { mappings: PropTypes.arrayOf(PropTypes.object), existingParams: PropTypes.arrayOf(PropTypes.object), onChange: PropTypes.func, }; static defaultProps = { mappings: [], existingParams: [], onChange: () => {}, }; static getStringValue(value) { // null if (!value) { return ""; } // range if (value instanceof Object && "start" in value && "end" in value) { return `${value.start} ~ ${value.end}`; } // just to be safe, array or object if (typeof value === "object") { return map(value, v => this.getStringValue(v)).join(", "); } // rest return value.toString(); } static getDefaultValue(mapping, existingParams) { const { type, mapTo, name } = mapping; let { param } = mapping; // if mapped to another param, swap 'em if (type === MappingType.DashboardMapToExisting && mapTo !== name) { const mappedTo = find(existingParams, { name: mapTo }); if (mappedTo) { // just being safe param = mappedTo; } // static type is different since it's fed param.normalizedValue } else if (type === MappingType.StaticValue) { param = param.clone().setValue(mapping.value); } let value = Parameter.getExecutionValue(param); // in case of dynamic value display the name instead of value if (param.hasDynamicValue) { value = param.normalizedValue.name; } return this.getStringValue(value); } static getSourceTypeLabel({ type, mapTo }) { switch (type) { case MappingType.DashboardAddNew: case MappingType.DashboardMapToExisting: return ( Dashboard {mapTo} ); case MappingType.WidgetLevel: return "Widget parameter"; case MappingType.StaticValue: return "Static value"; default: return ""; // won't happen (typescript-ftw) } } updateParamMapping(oldMapping, newMapping) { const mappings = [...this.props.mappings]; const index = findIndex(mappings, oldMapping); if (index >= 0) { // This should be the only possible case, but need to handle `else` too mappings[index] = newMapping; } else { mappings.push(newMapping); } this.props.onChange(mappings); } render() { const { existingParams } = this.props; // eslint-disable-line react/prop-types const dataSource = this.props.mappings.map(mapping => ({ mapping })); return (
`row${idx}`}> ( this.updateParamMapping(mapping, newMapping)} /> )} /> {`{{ ${mapping.name} }}`}} /> this.constructor.getDefaultValue(mapping, this.props.existingParams)} /> { const existingParamsNames = existingParams .filter(({ type }) => type === mapping.param.type) // exclude mismatching param types .map(({ name }) => name); // keep names only return ( {this.constructor.getSourceTypeLabel(mapping)}{" "} this.updateParamMapping(oldMapping, newMapping)} /> ); }} />
); } }