/* eslint-disable react/no-multi-comp */ import { isString, extend, each, 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 { clientConfig } from '@/services/auth'; import { Query, Parameter } from '@/services/query'; 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, clientConfig: PropTypes.any, // eslint-disable-line react/forbid-prop-types Query: PropTypes.any, // eslint-disable-line react/forbid-prop-types inputError: PropTypes.string, }; static defaultProps = { mapping: {}, existingParamNames: [], onChange: () => {}, clientConfig: null, Query: null, 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); 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 })} clientConfig={this.props.clientConfig} Query={this.props.Query} /> ); } 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() { 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); } const value = Parameter.getValue(param); 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)} /> ); }} />
); } }