in x-pack/platform/plugins/shared/lens/public/datasources/form_based/dimension_panel/dimension_editor.tsx [90:1222]
export function DimensionEditor(props: DimensionEditorProps) {
const {
selectedColumn,
operationSupportMatrix,
state,
columnId,
setState,
layerId,
currentIndexPattern,
hideGrouping,
dateRange,
dimensionGroups,
toggleFullscreen,
isFullscreen,
supportStaticValue,
enableFormatSelector = true,
layerType,
paramEditorCustomProps,
} = props;
const services = {
data: props.data,
fieldFormats: props.fieldFormats,
uiSettings: props.uiSettings,
http: props.http,
storage: props.storage,
unifiedSearch: props.unifiedSearch,
dataViews: props.dataViews,
};
const { fieldByOperation, operationWithoutField } = operationSupportMatrix;
const selectedOperationDefinition =
selectedColumn && operationDefinitionMap[selectedColumn.operationType];
const [temporaryState, setTemporaryState] = useState<TemporaryState>('none');
// If a layer has sampling disabled, assume the toast has already fired in the past
const [hasRandomSamplingToastFired, setSamplingToastAsFired] = useState(
!isSamplingValueEnabled(state.layers[layerId])
);
const [hasRankingToastFired, setRankingToastAsFired] = useState(false);
const [hasOtherBucketToastFired, setHasOtherBucketToastFired] = useState(false);
const temporaryQuickFunction = Boolean(temporaryState === quickFunctionsName);
const temporaryStaticValue = Boolean(temporaryState === staticValueOperationName);
const euiThemeContext = useEuiTheme();
const { euiTheme } = euiThemeContext;
const updateLayer = useCallback(
(newLayer: Partial<FormBasedLayer>) =>
setState((prevState) => mergeLayer({ state: prevState, layerId, newLayer })),
[layerId, setState]
);
const fireOrResetOtherBucketToast = useCallback(
(newLayer: FormBasedLayer) => {
if (isLayerChangingDueToOtherBucketChange(state.layers[layerId], newLayer)) {
props.notifications.toasts.add({
title: i18n.translate('xpack.lens.uiInfo.otherBucketChangeTitle', {
defaultMessage: '“Group remaining values as Other” disabled',
}),
text: i18n.translate('xpack.lens.uiInfo.otherBucketDisabled', {
defaultMessage:
'Values >= 1000 may slow performance. Re-enable the setting in “Advanced” options.',
}),
});
}
// resets the flag
setHasOtherBucketToastFired(!hasOtherBucketToastFired);
},
[layerId, props.notifications.toasts, state.layers, hasOtherBucketToastFired]
);
const fireOrResetRandomSamplingToast = useCallback(
(newLayer: FormBasedLayer) => {
// if prev and current sampling state is different, show a toast to the user
if (isSamplingValueEnabled(state.layers[layerId]) && !isSamplingValueEnabled(newLayer)) {
if (newLayer.sampling != null && newLayer.sampling < 1) {
props.notifications.toasts.add({
title: i18n.translate('xpack.lens.uiInfo.samplingDisabledTitle', {
defaultMessage: 'Layer sampling changed to 100%',
}),
text: i18n.translate('xpack.lens.uiInfo.samplingDisabledMessage', {
defaultMessage:
'The use of a maximum or minimum function on a layer requires all documents to be sampled in order to function properly.',
}),
});
}
}
// reset the flag if the user switches to another supported operation
setSamplingToastAsFired(!hasRandomSamplingToastFired);
},
[hasRandomSamplingToastFired, layerId, props.notifications.toasts, state.layers]
);
const fireOrResetRankingToast = useCallback(
(newLayer: FormBasedLayer) => {
if (isLayerChangingDueToDecimalsPercentile(state.layers[layerId], newLayer)) {
props.notifications.toasts.add({
title: i18n.translate('xpack.lens.uiInfo.rankingResetTitle', {
defaultMessage: 'Ranking changed to alphabetical',
}),
text: i18n.translate('xpack.lens.uiInfo.rankingResetToAlphabetical', {
defaultMessage: 'To rank by percentile, use whole numbers only.',
}),
});
}
// reset the flag if the user switches to another supported operation
setRankingToastAsFired(!hasRankingToastFired);
},
[hasRankingToastFired, layerId, props.notifications.toasts, state.layers]
);
const fireOrResetToastChecks = useCallback(
(newLayer: FormBasedLayer) => {
fireOrResetRandomSamplingToast(newLayer);
fireOrResetRankingToast(newLayer);
fireOrResetOtherBucketToast(newLayer);
},
[fireOrResetRandomSamplingToast, fireOrResetRankingToast, fireOrResetOtherBucketToast]
);
const setStateWrapper = useCallback(
(
setter:
| FormBasedLayer
| ((prevLayer: FormBasedLayer) => FormBasedLayer)
| GenericIndexPatternColumn,
options: { forceRender?: boolean } = {}
) => {
const layer = state.layers[layerId];
let hypotethicalLayer: FormBasedLayer;
if (isColumn(setter)) {
hypotethicalLayer = {
...layer,
columns: {
...layer.columns,
[columnId]: setter,
},
};
} else {
hypotethicalLayer = typeof setter === 'function' ? setter(state.layers[layerId]) : setter;
}
const isDimensionComplete = Boolean(hypotethicalLayer.columns[columnId]);
setState(
(prevState) => {
let outputLayer: FormBasedLayer;
const prevLayer = prevState.layers[layerId];
if (isColumn(setter)) {
outputLayer = {
...prevLayer,
columns: {
...prevLayer.columns,
[columnId]: setter,
},
};
} else {
outputLayer = typeof setter === 'function' ? setter(prevState.layers[layerId]) : setter;
}
const newLayer = adjustColumnReferencesForChangedColumn(outputLayer, columnId);
// Fire an info toast (eventually) on layer update
fireOrResetToastChecks(newLayer);
return mergeLayer({
state: prevState,
layerId,
newLayer,
});
},
{
isDimensionComplete,
...options,
}
);
},
[columnId, fireOrResetToastChecks, layerId, setState, state.layers]
);
const incompleteInfo = (state.layers[layerId].incompleteColumns ?? {})[columnId];
const {
operationType: incompleteOperation,
sourceField: incompleteField = null,
...incompleteParams
} = incompleteInfo || {};
const isQuickFunctionSelected = Boolean(
supportStaticValue
? selectedOperationDefinition && isQuickFunction(selectedOperationDefinition.type)
: !selectedOperationDefinition || isQuickFunction(selectedOperationDefinition.type)
);
const showQuickFunctions = temporaryQuickFunction || isQuickFunctionSelected;
const showStaticValueFunction =
temporaryStaticValue ||
(temporaryState === 'none' &&
supportStaticValue &&
(!selectedColumn || selectedColumn?.operationType === staticValueOperationName));
const addStaticValueColumn = (prevLayer = props.state.layers[props.layerId]) => {
if (selectedColumn?.operationType !== staticValueOperationName) {
const layer = insertOrReplaceColumn({
layer: prevLayer,
indexPattern: currentIndexPattern,
columnId,
op: staticValueOperationName,
visualizationGroups: dimensionGroups,
});
const value = props.activeData?.[layerId]?.rows[0]?.[columnId];
// replace the default value with the one from the active data
if (value != null) {
return updateDefaultLabels(
updateColumnParam({
layer,
columnId,
paramName: 'value',
value: props.activeData?.[layerId]?.rows[0]?.[columnId],
}),
currentIndexPattern
);
}
return layer;
}
return prevLayer;
};
// this function intercepts the state update for static value function
// and. if in temporary state, it merges the "add new static value column" state with the incoming
// changes from the static value operation (which has to be a function)
// Note: it forced a rerender at this point to avoid UI glitches in async updates (another hack upstream)
// TODO: revisit this once we get rid of updateDatasourceAsync upstream
const moveDefinitelyToStaticValueAndUpdate = (
setter:
| FormBasedLayer
| ((prevLayer: FormBasedLayer) => FormBasedLayer)
| GenericIndexPatternColumn
) => {
if (temporaryStaticValue) {
setTemporaryState('none');
}
if (typeof setter === 'function') {
return setState(
(prevState) => {
const layer = setter(addStaticValueColumn(prevState.layers[layerId]));
return mergeLayer({ state: prevState, layerId, newLayer: layer });
},
{
isDimensionComplete: true,
forceRender: true,
}
);
}
if (isColumn(setter)) {
throw new Error('static value should only be updated by the whole layer');
}
};
const ParamEditor = getParamEditor(
temporaryStaticValue,
selectedOperationDefinition,
supportStaticValue && !showQuickFunctions
);
const possibleOperations = useMemo(() => {
return Object.values(operationDefinitionMap)
.filter(({ hidden }) => !hidden)
.filter(
(operationDefinition) =>
!('selectionStyle' in operationDefinition) ||
operationDefinition.selectionStyle !== 'hidden'
)
.filter(({ type }) => fieldByOperation.get(type)?.size || operationWithoutField.has(type))
.sort((op1, op2) => {
return op1.displayName.localeCompare(op2.displayName);
})
.map((def) => def.type);
}, [fieldByOperation, operationWithoutField]);
const helpPopoverContainer = useRef<HTMLDivElement | null>(null);
useEffect(() => {
return () => {
if (helpPopoverContainer.current) {
ReactDOM.unmountComponentAtNode(helpPopoverContainer.current);
document.body.removeChild(helpPopoverContainer.current);
}
};
}, []);
const currentField =
selectedColumn &&
hasField(selectedColumn) &&
currentIndexPattern.getFieldByName(selectedColumn.sourceField);
const referencedField =
currentField || getReferencedField(selectedColumn, currentIndexPattern, state.layers[layerId]);
// Operations are compatible if they match inputs. They are always compatible in
// the empty state. Field-based operations are not compatible with field-less operations.
const operationsWithCompatibility = possibleOperations.map((operationType) => {
const definition = operationDefinitionMap[operationType];
return {
operationType,
compatibleWithCurrentField: canTransition({
layer: state.layers[layerId],
columnId,
op: operationType,
indexPattern: currentIndexPattern,
field: currentField || undefined,
filterOperations: props.filterOperations,
visualizationGroups: dimensionGroups,
dateRange,
}),
disabledStatus:
definition.getDisabledStatus &&
definition.getDisabledStatus(
props.indexPatterns[state.currentIndexPatternId],
state.layers[layerId],
layerType
),
compatibleWithSampling:
getSamplingValue(state.layers[layerId]) === 1 ||
(definition.getUnsupportedSettings?.()?.sampling ?? true),
documentation: definition.quickFunctionDocumentation,
};
});
const currentFieldIsInvalid = useMemo(
() => fieldIsInvalid(state.layers[layerId], columnId, currentIndexPattern),
[state.layers, layerId, columnId, currentIndexPattern]
);
const shouldDisplayDots =
temporaryState === 'none' ||
(selectedColumn?.operationType != null && isQuickFunction(selectedColumn?.operationType));
const sideNavItems: EuiListGroupItemProps[] = operationsWithCompatibility.map(
({
operationType,
compatibleWithCurrentField,
disabledStatus,
compatibleWithSampling,
documentation,
}) => {
const isActive = Boolean(
incompleteOperation === operationType ||
(!incompleteOperation && selectedColumn && selectedColumn.operationType === operationType)
);
const partialIcon = compatibleWithCurrentField &&
referencedField?.partiallyApplicableFunctions?.[operationType] && (
<span data-test-subj={`${operationType}-partial-warning`}>
{' '}
<EuiIconTip
content={i18n.translate(
'xpack.lens.indexPattern.helpPartiallyApplicableFunctionLabel',
{
defaultMessage:
'This function may only return partial results, as it is unable to support the full time range of rolled-up historical data.',
}
)}
position="left"
size="s"
type="partial"
color="warning"
/>
</span>
);
let label: EuiListGroupItemProps['label'] = (
<>
{operationDisplay[operationType].displayName}
{partialIcon}
</>
);
if (isActive && disabledStatus) {
label = (
<EuiToolTip content={disabledStatus} display="block" position="left">
<EuiText color="danger" size="s">
<strong>{label}</strong>
</EuiText>
</EuiToolTip>
);
} else if (disabledStatus) {
label = (
<EuiToolTip content={disabledStatus} display="block" position="left">
<span>{operationDisplay[operationType].displayName}</span>
</EuiToolTip>
);
} else if (!compatibleWithCurrentField) {
label = (
<EuiFlexGroup gutterSize="none" alignItems="center" responsive={false}>
<EuiFlexItem grow={false} style={{ marginRight: euiTheme.size.xs, minWidth: 0 }}>
<span
css={css`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`}
>
{label}
</span>
</EuiFlexItem>
{shouldDisplayDots && (
<EuiFlexItem grow={false}>
<EuiIconTip
content={i18n.translate('xpack.lens.indexPattern.helpIncompatibleFieldDotLabel', {
defaultMessage:
'This function is not compatible with the current selected field',
})}
position="left"
size="s"
type="dot"
color={euiTheme.colors.warning}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
} else if (!compatibleWithSampling) {
label = (
<EuiFlexGroup gutterSize="none" alignItems="center" responsive={false}>
<EuiFlexItem grow={false} style={{ marginRight: euiTheme.size.xs }}>
{label}
</EuiFlexItem>
{shouldDisplayDots && (
<EuiFlexItem grow={false}>
<EuiIconTip
content={i18n.translate('xpack.lens.indexPattern.settingsSamplingUnsupported', {
defaultMessage: `Selecting this function will change this layer's sampling to 100% in order to function properly.`,
})}
size="s"
type="dot"
color="warning"
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
}
return {
id: operationType as string,
label,
isActive,
size: 's',
isDisabled: !!disabledStatus,
css: operationsButtonStyles(euiThemeContext),
'data-test-subj': `lns-indexPatternDimension-${operationType}${
compatibleWithCurrentField ? '' : ' incompatible'
}`,
[`aria-pressed`]: isActive,
extraAction: operationDefinitionMap[operationType].helpComponent
? {
color: 'primary',
onClick: (e) => {
if (!helpPopoverContainer.current) {
const container = document.createElement('div');
helpPopoverContainer.current = container;
document.body.appendChild(container);
const HelpComponent = operationDefinitionMap[operationType].helpComponent!;
const element = (
<WrappingHelpPopover
button={e.target as HTMLElement}
isOpen={true}
title={operationDefinitionMap[operationType].helpComponentTitle}
closePopover={() => {
if (helpPopoverContainer.current) {
ReactDOM.unmountComponentAtNode(helpPopoverContainer.current);
document.body.removeChild(helpPopoverContainer.current);
helpPopoverContainer.current = null;
}
}}
startServices={props.core}
>
<HelpComponent />
</WrappingHelpPopover>
);
ReactDOM.render(element, helpPopoverContainer.current);
} else {
ReactDOM.unmountComponentAtNode(helpPopoverContainer.current);
document.body.removeChild(helpPopoverContainer.current);
helpPopoverContainer.current = null;
}
},
iconType: 'documentation',
iconSize: 's',
'aria-label': i18n.translate('xpack.lens.indexPattern.helpLabel', {
defaultMessage: 'Function help',
}),
}
: undefined,
showToolTip: !disabledStatus,
toolTipProps: {
position: 'left',
},
toolTipText: documentation,
onClick() {
if (
['none', 'fullReference', 'managedReference'].includes(
operationDefinitionMap[operationType].input
)
) {
// Clear invalid state because we are reseting to a valid column
if (selectedColumn?.operationType === operationType) {
if (incompleteInfo) {
setStateWrapper(resetIncomplete(state.layers[layerId], columnId));
}
return;
}
const newLayer = insertOrReplaceColumn({
layer: props.state.layers[props.layerId],
indexPattern: currentIndexPattern,
columnId,
op: operationType,
visualizationGroups: dimensionGroups,
targetGroup: props.groupId,
});
if (
temporaryQuickFunction &&
isQuickFunction(newLayer.columns[columnId].operationType)
) {
// Only switch the tab once the "non quick function" is fully removed
setTemporaryState('none');
}
setStateWrapper(newLayer);
return;
} else if (!selectedColumn || !compatibleWithCurrentField) {
const possibleFields = fieldByOperation.get(operationType) ?? new Set<string>();
let newLayer: FormBasedLayer;
if (possibleFields.size === 1) {
newLayer = insertOrReplaceColumn({
layer: props.state.layers[props.layerId],
indexPattern: currentIndexPattern,
columnId,
op: operationType,
field: currentIndexPattern.getFieldByName(possibleFields.values().next().value),
visualizationGroups: dimensionGroups,
targetGroup: props.groupId,
});
} else {
newLayer = insertOrReplaceColumn({
layer: props.state.layers[props.layerId],
indexPattern: currentIndexPattern,
columnId,
op: operationType,
// if document field can be used, default to it
field: possibleFields.has(DOCUMENT_FIELD_NAME) ? documentField : undefined,
visualizationGroups: dimensionGroups,
targetGroup: props.groupId,
});
}
if (
temporaryQuickFunction &&
isQuickFunction(newLayer.columns[columnId].operationType)
) {
// Only switch the tab once the "non quick function" is fully removed
setTemporaryState('none');
}
setStateWrapper(newLayer);
return;
}
if (selectedColumn.operationType === operationType) {
if (incompleteInfo) {
setStateWrapper(resetIncomplete(state.layers[layerId], columnId));
}
return;
}
if (temporaryQuickFunction) {
setTemporaryState('none');
}
const newLayer = replaceColumn({
layer: props.state.layers[props.layerId],
indexPattern: currentIndexPattern,
columnId,
op: operationType,
field: hasField(selectedColumn)
? currentIndexPattern.getFieldByName(selectedColumn.sourceField)
: undefined,
visualizationGroups: dimensionGroups,
});
setStateWrapper(newLayer);
},
};
}
);
const shouldDisplayExtraOptions =
!currentFieldIsInvalid &&
!incompleteInfo &&
selectedColumn &&
isQuickFunction(selectedColumn.operationType) &&
ParamEditor;
const shouldDisplayReferenceEditor =
!incompleteInfo &&
selectedColumn &&
'references' in selectedColumn &&
selectedOperationDefinition?.input === 'fullReference';
const shouldDisplayFieldInput =
!selectedColumn ||
selectedOperationDefinition?.input === 'field' ||
(incompleteOperation && operationDefinitionMap[incompleteOperation]?.input === 'field') ||
temporaryQuickFunction;
const FieldInputComponent = selectedOperationDefinition?.renderFieldInput || FieldInput;
const paramEditorProps: ParamEditorProps<
GenericIndexPatternColumn,
FormBasedLayer | ((prevLayer: FormBasedLayer) => FormBasedLayer) | GenericIndexPatternColumn
> = {
layer: state.layers[layerId],
layerId,
activeData: props.activeData,
paramEditorUpdater: (setter) => {
if (temporaryQuickFunction) {
setTemporaryState('none');
}
setStateWrapper(setter, { forceRender: temporaryQuickFunction });
},
columnId,
currentColumn: state.layers[layerId].columns[columnId],
dateRange,
indexPattern: currentIndexPattern,
operationDefinitionMap,
toggleFullscreen,
isFullscreen,
paramEditorCustomProps,
ReferenceEditor,
dataSectionExtra: props.dataSectionExtra,
...services,
};
const quickFunctions = (
<>
<EuiFormRow
label={
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
{i18n.translate('xpack.lens.indexPattern.functionsLabel', {
defaultMessage: 'Functions',
})}
</EuiFlexItem>
</EuiFlexGroup>
}
fullWidth
>
<EuiListGroup
css={sideNavItems.length > 3 ? operationsTwoColumnsStyles(euiThemeContext) : undefined}
gutterSize="none"
color="primary"
listItems={
// add a padding item containing a non breakable space if the number of operations is not even
// otherwise the column layout will break within an element
sideNavItems.length % 2 === 1 ? [...sideNavItems, { label: '\u00a0' }] : sideNavItems
}
maxWidth={false}
/>
</EuiFormRow>
{shouldDisplayReferenceEditor ? (
<>
{selectedColumn.references.map((referenceId, index) => {
const validation = selectedOperationDefinition.requiredReferences[index];
const layer = state.layers[layerId];
return (
<ReferenceEditor
operationDefinitionMap={operationDefinitionMap}
key={index}
layer={layer}
layerId={layerId}
activeData={props.activeData}
columnId={referenceId}
column={layer.columns[referenceId]}
incompleteColumn={
layer.incompleteColumns ? layer.incompleteColumns[referenceId] : undefined
}
onResetIncomplete={() => {
updateLayer({
...layer,
// clean up the incomplete column data for the referenced id
incompleteColumns: { ...layer.incompleteColumns, [referenceId]: undefined },
});
}}
onDeleteColumn={() => {
updateLayer(
deleteColumn({
layer,
columnId: referenceId,
indexPattern: currentIndexPattern,
})
);
}}
onChooseFunction={(operationType: string, field?: IndexPatternField) => {
const newLayer = insertOrReplaceColumn({
layer,
columnId: referenceId,
op: operationType,
indexPattern: currentIndexPattern,
field,
visualizationGroups: dimensionGroups,
});
fireOrResetToastChecks(newLayer);
updateLayer(newLayer);
}}
onChooseField={(choice: FieldChoiceWithOperationType) => {
updateLayer(
insertOrReplaceColumn({
layer,
columnId: referenceId,
indexPattern: currentIndexPattern,
op: choice.operationType,
field: currentIndexPattern.getFieldByName(choice.field),
visualizationGroups: dimensionGroups,
})
);
}}
paramEditorUpdater={(
setter:
| FormBasedLayer
| ((prevLayer: FormBasedLayer) => FormBasedLayer)
| GenericIndexPatternColumn
) => {
let newLayer: FormBasedLayer;
if (typeof setter === 'function') {
newLayer = setter(layer);
} else if (isColumn(setter)) {
newLayer = {
...layer,
columns: {
...layer.columns,
[referenceId]: setter,
},
};
} else {
newLayer = setter;
}
fireOrResetToastChecks(newLayer);
return updateLayer(adjustColumnReferencesForChangedColumn(newLayer, referenceId));
}}
validation={validation}
currentIndexPattern={currentIndexPattern}
selectionStyle={selectedOperationDefinition.selectionStyle}
dateRange={dateRange}
labelAppend={selectedOperationDefinition?.getHelpMessage?.({
data: props.data,
uiSettings: props.uiSettings,
currentColumn: layer.columns[columnId],
})}
isFullscreen={isFullscreen}
toggleFullscreen={toggleFullscreen}
paramEditorCustomProps={paramEditorCustomProps}
{...services}
/>
);
})}
{selectedOperationDefinition.selectionStyle !== 'field' ? <EuiSpacer size="s" /> : null}
</>
) : null}
{shouldDisplayFieldInput ? (
<FieldInputComponent
layer={state.layers[layerId]}
selectedColumn={selectedColumn as FieldBasedIndexPatternColumn}
columnId={columnId}
indexPattern={currentIndexPattern}
operationSupportMatrix={operationSupportMatrix}
updateLayer={(newLayer) => {
if (temporaryQuickFunction) {
setTemporaryState('none');
}
setStateWrapper(newLayer, { forceRender: temporaryQuickFunction });
}}
incompleteField={incompleteField}
incompleteOperation={incompleteOperation}
incompleteParams={incompleteParams}
currentFieldIsInvalid={currentFieldIsInvalid}
helpMessage={selectedOperationDefinition?.getHelpMessage?.({
data: props.data,
uiSettings: props.uiSettings,
currentColumn: state.layers[layerId].columns[columnId],
})}
dimensionGroups={dimensionGroups}
groupId={props.groupId}
operationDefinitionMap={operationDefinitionMap}
/>
) : null}
{!isFullscreen && !incompleteInfo && !hideGrouping && temporaryState === 'none' && (
<BucketNestingEditor
layer={state.layers[props.layerId]}
columnId={props.columnId}
setColumns={(columnOrder) => updateLayer({ columnOrder })}
getFieldByName={currentIndexPattern.getFieldByName}
/>
)}
{shouldDisplayExtraOptions && <ParamEditor {...paramEditorProps} />}
{!selectedOperationDefinition?.handleDataSectionExtra && (
<>
<EuiSpacer size="m" />
{props.dataSectionExtra}
</>
)}
</>
);
const customParamEditor = ParamEditor ? (
<>
<ParamEditor
layer={state.layers[layerId]}
activeData={props.activeData}
paramEditorUpdater={
temporaryStaticValue ? moveDefinitelyToStaticValueAndUpdate : setStateWrapper
}
columnId={columnId}
currentColumn={state.layers[layerId].columns[columnId]}
operationDefinitionMap={operationDefinitionMap}
layerId={layerId}
paramEditorCustomProps={paramEditorCustomProps}
dateRange={dateRange}
isFullscreen={isFullscreen}
indexPattern={currentIndexPattern}
toggleFullscreen={toggleFullscreen}
ReferenceEditor={ReferenceEditor}
{...services}
/>
</>
) : null;
const ButtonGroupContent = showQuickFunctions ? quickFunctions : customParamEditor;
const onFormatChange = useCallback<FormatSelectorProps['onChange']>(
(newFormat) => {
updateLayer(
updateColumnParam({
layer: state.layers[layerId],
columnId,
paramName: 'format',
value: newFormat,
})
);
},
[columnId, layerId, state.layers, updateLayer]
);
const hasFormula =
!isFullscreen && operationSupportMatrix.operationWithoutField.has(formulaOperationName);
const hasButtonGroups = !isFullscreen && (hasFormula || supportStaticValue);
const initialMethod = useMemo(() => {
let methodId = '';
if (showStaticValueFunction) {
methodId = staticValueOperationName;
} else if (showQuickFunctions) {
methodId = quickFunctionsName;
} else if (
temporaryState === 'none' &&
selectedColumn?.operationType === formulaOperationName
) {
methodId = formulaOperationName;
}
return methodId;
}, [selectedColumn?.operationType, showQuickFunctions, showStaticValueFunction, temporaryState]);
const [selectedMethod, setSelectedMethod] = useState(initialMethod);
const options: DimensionEditorGroupsOptions[] = [
{
id: staticValueOperationName,
enabled: Boolean(supportStaticValue),
state: showStaticValueFunction,
onClick: () => {
if (selectedColumn?.operationType === formulaOperationName) {
return setTemporaryState(staticValueOperationName);
}
setTemporaryState('none');
setStateWrapper(addStaticValueColumn());
return;
},
label: i18n.translate('xpack.lens.indexPattern.staticValueLabel', {
defaultMessage: 'Static value',
}),
},
{
id: quickFunctionsName,
enabled: true,
state: showQuickFunctions,
onClick: () => {
if (selectedColumn && !isQuickFunction(selectedColumn.operationType)) {
setTemporaryState(quickFunctionsName);
return;
}
},
label: i18n.translate('xpack.lens.indexPattern.quickFunctionsLabel', {
defaultMessage: 'Quick function',
}),
},
{
id: formulaOperationName,
enabled: hasFormula,
state: temporaryState === 'none' && selectedColumn?.operationType === formulaOperationName,
onClick: () => {
setTemporaryState('none');
if (selectedColumn?.operationType !== formulaOperationName) {
const newLayer = insertOrReplaceColumn({
layer: props.state.layers[props.layerId],
indexPattern: currentIndexPattern,
columnId,
op: formulaOperationName,
visualizationGroups: dimensionGroups,
});
setStateWrapper(newLayer);
}
},
label: i18n.translate('xpack.lens.indexPattern.formulaLabel', {
defaultMessage: 'Formula',
}),
},
];
const defaultLabel = useMemo(
() =>
String(
selectedColumn &&
operationDefinitionMap[selectedColumn.operationType].getDefaultLabel(
selectedColumn,
state.layers[layerId].columns,
props.indexPatterns[state.layers[layerId].indexPatternId]
)
),
[layerId, selectedColumn, props.indexPatterns, state.layers]
);
/**
* Advanced options can cause side effects on other columns (i.e. formulas)
* so before updating the layer the full insertOrReplaceColumn needs to be performed
*/
const updateAdvancedOption = useCallback<TimeScalingProps['updateLayer']>(
(newLayer) => {
if (selectedColumn) {
setStateWrapper(
// formula need to regenerate from scratch
selectedColumn.operationType === formulaOperationName
? insertOrReplaceColumn({
op: selectedColumn.operationType,
layer: newLayer,
columnId,
indexPattern: currentIndexPattern,
visualizationGroups: dimensionGroups,
})
: newLayer
);
}
},
[columnId, currentIndexPattern, dimensionGroups, selectedColumn, setStateWrapper]
);
const shouldDisplayAdvancedOptions =
!isFullscreen &&
!currentFieldIsInvalid &&
!incompleteInfo &&
selectedColumn &&
temporaryState === 'none' &&
selectedOperationDefinition &&
(selectedOperationDefinition.timeScalingMode ||
selectedOperationDefinition.filterable ||
selectedOperationDefinition.shiftable);
return (
<div id={columnId}>
<div className="lnsIndexPatternDimensionEditor--padded">
<EuiText
size="s"
css={css`
margin-bottom: ${euiTheme.size.base};
`}
>
<h4>
{paramEditorCustomProps?.headingLabel ??
i18n.translate('xpack.lens.indexPattern.dimensionEditor.headingData', {
defaultMessage: 'Data',
})}
</h4>
</EuiText>
<>
{hasButtonGroups ? (
<DimensionEditorButtonGroups
options={options}
onMethodChange={(optionId: string) => {
setSelectedMethod(optionId);
}}
selectedMethod={selectedMethod}
/>
) : null}
<CalloutWarning
currentOperationType={selectedColumn?.operationType}
temporaryStateType={temporaryState}
/>
{ButtonGroupContent}
</>
</div>
{shouldDisplayAdvancedOptions && (
<AdvancedOptions
options={[
{
dataTestSubj: 'indexPattern-time-scaling-enable',
inlineElement: selectedOperationDefinition.timeScalingMode ? (
<TimeScaling
selectedColumn={selectedColumn}
columnId={columnId}
layer={state.layers[layerId]}
updateLayer={updateAdvancedOption}
/>
) : null,
},
{
dataTestSubj: 'indexPattern-filter-by-enable',
inlineElement: selectedOperationDefinition.filterable ? (
<Filtering
indexPattern={currentIndexPattern}
selectedColumn={selectedColumn}
columnId={columnId}
layer={state.layers[layerId]}
updateLayer={updateAdvancedOption}
helpMessage={getHelpMessage(selectedOperationDefinition.filterable)}
/>
) : null,
},
{
dataTestSubj: 'indexPattern-reducedTimeRange-enable',
inlineElement: selectedOperationDefinition.canReduceTimeRange ? (
<ReducedTimeRange
selectedColumn={selectedColumn}
columnId={columnId}
indexPattern={currentIndexPattern}
layer={state.layers[layerId]}
updateLayer={updateAdvancedOption}
skipLabelUpdate={hasFormula}
helpMessage={getHelpMessage(selectedOperationDefinition.canReduceTimeRange)}
/>
) : null,
},
{
dataTestSubj: 'indexPattern-time-shift-enable',
inlineElement: Boolean(
selectedOperationDefinition.shiftable &&
(currentIndexPattern.timeFieldName ||
Object.values(state.layers[layerId].columns).some(
(col) => col.operationType === 'date_histogram'
))
) ? (
<TimeShift
datatableUtilities={services.data.datatableUtilities}
indexPattern={currentIndexPattern}
selectedColumn={selectedColumn}
columnId={columnId}
layer={state.layers[layerId]}
updateLayer={updateAdvancedOption}
activeData={props.activeData}
layerId={layerId}
/>
) : null,
},
...(operationDefinitionMap[selectedColumn.operationType].getAdvancedOptions?.(
paramEditorProps
) || []),
]}
/>
)}
{!isFullscreen && !currentFieldIsInvalid && (
<div className="lnsIndexPatternDimensionEditor--padded lnsIndexPatternDimensionEditor--collapseNext">
{!incompleteInfo && temporaryState === 'none' && selectedColumn && (
<EuiText
size="s"
css={css`
margin-bottom: ${euiTheme.size.base};
`}
>
<h4>
{i18n.translate('xpack.lens.indexPattern.dimensionEditor.headingAppearance', {
defaultMessage: 'Appearance',
})}
</h4>
</EuiText>
)}
<>
{!incompleteInfo && selectedColumn && temporaryState === 'none' && (
<NameInput
value={selectedColumn.label}
defaultValue={defaultLabel}
onChange={(value) => {
updateLayer({
columns: {
...state.layers[layerId].columns,
[columnId]: {
...selectedColumn,
label: value,
customLabel:
operationDefinitionMap[selectedColumn.operationType].getDefaultLabel(
selectedColumn,
state.layers[layerId].columns,
props.indexPatterns[state.layers[layerId].indexPatternId]
) !== value,
},
},
});
}}
/>
)}
{enableFormatSelector &&
!isFullscreen &&
selectedColumn &&
(selectedColumn.dataType === 'number' || selectedColumn.operationType === 'range') ? (
<FormatSelector
selectedColumn={selectedColumn}
onChange={onFormatChange}
docLinks={props.core.docLinks}
/>
) : null}
</>
</div>
)}
</div>
);
}