mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -04:00
[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:
parent
210280580b
commit
531d80aae4
5 changed files with 121 additions and 13 deletions
|
@ -113,8 +113,15 @@ const DataGrid: React.FC<ESQLDataGridProps> = (props) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const CellValueRenderer = useMemo(() => {
|
const CellValueRenderer = useMemo(() => {
|
||||||
return getCellValueRenderer(rows, editingCell, savingDocs, setEditingCell, onValueChange);
|
return getCellValueRenderer(
|
||||||
}, [rows, editingCell, setEditingCell, onValueChange, savingDocs]);
|
rows,
|
||||||
|
props.columns,
|
||||||
|
editingCell,
|
||||||
|
savingDocs,
|
||||||
|
setEditingCell,
|
||||||
|
onValueChange
|
||||||
|
);
|
||||||
|
}, [rows, props.columns, editingCell, setEditingCell, onValueChange, savingDocs]);
|
||||||
|
|
||||||
const externalCustomRenderers: CustomCellRenderer = useMemo(() => {
|
const externalCustomRenderers: CustomCellRenderer = useMemo(() => {
|
||||||
return activeColumns.reduce((acc, columnId) => {
|
return activeColumns.reduce((acc, columnId) => {
|
||||||
|
|
|
@ -69,7 +69,7 @@ export const RowColumnCreator = ({ columns }: { columns: DatatableColumn[] }) =>
|
||||||
onChange={updateRow(column.id)}
|
onChange={updateRow(column.id)}
|
||||||
autoFocus={index === 0}
|
autoFocus={index === 0}
|
||||||
css={css`
|
css={css`
|
||||||
min-width: ${230}px;
|
min-width: ${180}px;
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
|
|
@ -7,13 +7,17 @@
|
||||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EuiFieldText } from '@elastic/eui';
|
import React, { useMemo, useState } from 'react';
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
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 {
|
interface ValueInputProps {
|
||||||
value?: string;
|
value?: string;
|
||||||
columnName?: string;
|
columnName?: string;
|
||||||
|
columns?: DatatableColumn[];
|
||||||
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
|
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
|
||||||
onEnter?: (value: string) => void;
|
onEnter?: (value: string) => void;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
|
@ -24,33 +28,55 @@ interface ValueInputProps {
|
||||||
export const ValueInput = ({
|
export const ValueInput = ({
|
||||||
value = '',
|
value = '',
|
||||||
columnName = '',
|
columnName = '',
|
||||||
|
columns,
|
||||||
onBlur,
|
onBlur,
|
||||||
onEnter,
|
onEnter,
|
||||||
onChange,
|
onChange,
|
||||||
autoFocus = false,
|
autoFocus = false,
|
||||||
className = '',
|
className = '',
|
||||||
}: ValueInputProps) => {
|
}: ValueInputProps) => {
|
||||||
|
const {
|
||||||
|
services: { notifications },
|
||||||
|
} = useKibana<KibanaContextExtra>();
|
||||||
const [editValue, setEditValue] = useState(value);
|
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>) => {
|
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
// Perform Validations
|
if (error) {
|
||||||
|
notifications.toasts.addDanger({
|
||||||
|
title: error,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
onEnter?.(editValue);
|
onEnter?.(editValue);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBlurHandler = (event: React.FocusEvent<HTMLInputElement>) => {
|
const onBlurHandler = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||||
if (onBlur) {
|
if (error) {
|
||||||
onBlur(event);
|
notifications.toasts.addDanger({
|
||||||
|
title: error,
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// Perform Validations ?
|
onBlur?.(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const InputComponent = useMemo(() => getInputComponentForType(columnType), [columnType]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiFieldText
|
<InputComponent
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
compressed
|
|
||||||
placeholder={columnName}
|
placeholder={columnName}
|
||||||
|
label={columnName}
|
||||||
value={editValue}
|
value={editValue}
|
||||||
aria-label={i18n.translate('indexEditor.cellValueInput.aria', {
|
aria-label={i18n.translate('indexEditor.cellValueInput.aria', {
|
||||||
defaultMessage: 'Value for {columnName}',
|
defaultMessage: 'Value for {columnName}',
|
||||||
|
@ -60,6 +86,7 @@ export const ValueInput = ({
|
||||||
setEditValue(e.target.value);
|
setEditValue(e.target.value);
|
||||||
onChange?.(e.target.value);
|
onChange?.(e.target.value);
|
||||||
}}
|
}}
|
||||||
|
onError={setError}
|
||||||
onBlur={onBlurHandler}
|
onBlur={onBlurHandler}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
className={className}
|
className={className}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||||
import { type DataGridCellValueElementProps } from '@kbn/unified-data-table';
|
import { type DataGridCellValueElementProps } from '@kbn/unified-data-table';
|
||||||
import type { DataTableRecord } from '@kbn/discover-utils';
|
import type { DataTableRecord } from '@kbn/discover-utils';
|
||||||
import { isNil } from 'lodash';
|
import { isNil } from 'lodash';
|
||||||
|
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||||
import type { PendingSave } from '../index_update_service';
|
import type { PendingSave } from '../index_update_service';
|
||||||
import { ValueInput } from './value_input';
|
import { ValueInput } from './value_input';
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ export type OnCellValueChange = (docId: string, update: any) => void;
|
||||||
export const getCellValueRenderer =
|
export const getCellValueRenderer =
|
||||||
(
|
(
|
||||||
rows: DataTableRecord[],
|
rows: DataTableRecord[],
|
||||||
|
columns: DatatableColumn[],
|
||||||
editingCell: { row: number | null; col: string | null },
|
editingCell: { row: number | null; col: string | null },
|
||||||
savingDocs: PendingSave | undefined,
|
savingDocs: PendingSave | undefined,
|
||||||
onEditStart: (update: { row: number | null; col: string | null }) => void,
|
onEditStart: (update: { row: number | null; col: string | null }) => void,
|
||||||
|
@ -40,9 +42,8 @@ export const getCellValueRenderer =
|
||||||
cellValue = pendingSaveValue;
|
cellValue = pendingSaveValue;
|
||||||
} else if (row.flattened) {
|
} else if (row.flattened) {
|
||||||
// Otherwise, use the value from the row
|
// Otherwise, use the value from the row
|
||||||
cellValue = row.flattened[columnId];
|
cellValue = row.flattened[columnId]?.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cellValue == null) {
|
if (cellValue == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -60,6 +61,7 @@ export const getCellValueRenderer =
|
||||||
onValueChange(docId!, { [columnId]: value });
|
onValueChange(docId!, { [columnId]: value });
|
||||||
}}
|
}}
|
||||||
columnName={columnId}
|
columnName={columnId}
|
||||||
|
columns={columns}
|
||||||
value={cellValue}
|
value={cellValue}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue