[ES|QL] Index editor cell validations (#224728)

## Summary
Related issue https://github.com/elastic/kibana/issues/207330

Adds simple type validations when editing/adding a new cell in the index
editor.

<img width="1271" alt="image"
src="https://github.com/user-attachments/assets/6ec9fae4-f0d6-4ab2-b32e-64dc1594c36a"
/>
This commit is contained in:
Sebastian Delle Donne 2025-06-26 09:21:32 +02:00 committed by GitHub
parent 210280580b
commit 531d80aae4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 121 additions and 13 deletions

View file

@ -113,8 +113,15 @@ const DataGrid: React.FC<ESQLDataGridProps> = (props) => {
);
const CellValueRenderer = useMemo(() => {
return getCellValueRenderer(rows, editingCell, savingDocs, setEditingCell, onValueChange);
}, [rows, editingCell, setEditingCell, onValueChange, savingDocs]);
return getCellValueRenderer(
rows,
props.columns,
editingCell,
savingDocs,
setEditingCell,
onValueChange
);
}, [rows, props.columns, editingCell, setEditingCell, onValueChange, savingDocs]);
const externalCustomRenderers: CustomCellRenderer = useMemo(() => {
return activeColumns.reduce((acc, columnId) => {

View file

@ -69,7 +69,7 @@ export const RowColumnCreator = ({ columns }: { columns: DatatableColumn[] }) =>
onChange={updateRow(column.id)}
autoFocus={index === 0}
css={css`
min-width: ${230}px;
min-width: ${180}px;
`}
/>
</EuiFlexItem>

View file

@ -7,13 +7,17 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EuiFieldText } from '@elastic/eui';
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { getInputComponentForType } from './value_inputs_factory';
import { KibanaContextExtra } from '../types';
interface ValueInputProps {
value?: string;
columnName?: string;
columns?: DatatableColumn[];
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
onEnter?: (value: string) => void;
onChange?: (value: string) => void;
@ -24,33 +28,55 @@ interface ValueInputProps {
export const ValueInput = ({
value = '',
columnName = '',
columns,
onBlur,
onEnter,
onChange,
autoFocus = false,
className = '',
}: ValueInputProps) => {
const {
services: { notifications },
} = useKibana<KibanaContextExtra>();
const [editValue, setEditValue] = useState(value);
const [error, setError] = useState<string | null>(null);
const columnType = useMemo(() => {
if (!columns || !columnName) return;
const col = columns.find((c) => c.name === columnName);
return col?.meta?.type;
}, [columns, columnName]);
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
// Perform Validations
if (error) {
notifications.toasts.addDanger({
title: error,
});
return;
}
onEnter?.(editValue);
}
};
const onBlurHandler = (event: React.FocusEvent<HTMLInputElement>) => {
if (onBlur) {
onBlur(event);
if (error) {
notifications.toasts.addDanger({
title: error,
});
return;
}
// Perform Validations ?
onBlur?.(event);
};
const InputComponent = useMemo(() => getInputComponentForType(columnType), [columnType]);
return (
<EuiFieldText
<InputComponent
autoFocus={autoFocus}
compressed
placeholder={columnName}
label={columnName}
value={editValue}
aria-label={i18n.translate('indexEditor.cellValueInput.aria', {
defaultMessage: 'Value for {columnName}',
@ -60,6 +86,7 @@ export const ValueInput = ({
setEditValue(e.target.value);
onChange?.(e.target.value);
}}
onError={setError}
onBlur={onBlurHandler}
onKeyDown={onKeyDown}
className={className}

View file

@ -12,6 +12,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { type DataGridCellValueElementProps } from '@kbn/unified-data-table';
import type { DataTableRecord } from '@kbn/discover-utils';
import { isNil } from 'lodash';
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import type { PendingSave } from '../index_update_service';
import { ValueInput } from './value_input';
@ -20,6 +21,7 @@ export type OnCellValueChange = (docId: string, update: any) => void;
export const getCellValueRenderer =
(
rows: DataTableRecord[],
columns: DatatableColumn[],
editingCell: { row: number | null; col: string | null },
savingDocs: PendingSave | undefined,
onEditStart: (update: { row: number | null; col: string | null }) => void,
@ -40,9 +42,8 @@ export const getCellValueRenderer =
cellValue = pendingSaveValue;
} else if (row.flattened) {
// Otherwise, use the value from the row
cellValue = row.flattened[columnId];
cellValue = row.flattened[columnId]?.toString();
}
if (cellValue == null) {
return null;
}
@ -60,6 +61,7 @@ export const getCellValueRenderer =
onValueChange(docId!, { [columnId]: value });
}}
columnName={columnId}
columns={columns}
value={cellValue}
autoFocus
/>

View file

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useCallback } from 'react';
import { EuiFieldText, EuiFieldNumber } from '@elastic/eui';
import { DatatableColumnType } from '@kbn/expressions-plugin/common';
import { i18n } from '@kbn/i18n';
export interface ValueInputProps {
onError?: (error: string | null) => void;
value: string;
label?: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
autoFocus?: boolean;
className?: string;
isInvalid?: boolean;
placeholder?: string;
}
export const StringInput: React.FC<ValueInputProps> = ({ onError, ...restOfProps }) => (
<EuiFieldText compressed {...restOfProps} />
);
export const NumberInput: React.FC<ValueInputProps> = ({ onError, ...restOfProps }) => (
<EuiFieldNumber compressed {...restOfProps} />
);
export const BooleanInput: React.FC<ValueInputProps> = ({ onError, onChange, ...restOfProps }) => {
const [error, setError] = React.useState<string | null>(null);
const onChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (!['true', 'false'].includes(e.target.value.toLowerCase())) {
const booleanError = i18n.translate('indexEditor.cellValueInput.validation.boolean', {
defaultMessage: 'Value must be true or false',
});
setError(booleanError);
onError?.(booleanError);
} else {
setError(null);
onError?.(null);
}
onChange?.(e);
},
[onError, onChange]
);
return (
<EuiFieldText compressed {...restOfProps} onChange={onChangeHandler} isInvalid={!!error} />
);
};
export function getInputComponentForType(
type: DatatableColumnType | undefined
): React.FC<ValueInputProps> {
switch (type) {
case 'number':
return NumberInput;
case 'boolean':
return BooleanInput;
default:
return StringInput;
}
}