Skip to content

Commit edf2e02

Browse files
Merge branch 'main' into 239385-fs-usage-adm-zip
2 parents 5d54116 + fe9f869 commit edf2e02

56 files changed

Lines changed: 1804 additions & 406 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/platform/packages/private/kbn-index-editor/moon.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ dependsOn:
4545
- '@kbn/esql-utils'
4646
- '@kbn/test-jest-helpers'
4747
- '@kbn/esql-types'
48+
- '@kbn/field-types'
49+
- '@kbn/field-utils'
4850
tags:
4951
- shared-browser
5052
- package

src/platform/packages/private/kbn-index-editor/src/components/data_grid.tsx

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@ import type { DataTableColumnsMeta, DataTableRecord } from '@kbn/discover-utils/
1212
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
1313
import { useKibana } from '@kbn/kibana-react-plugin/public';
1414
import { css } from '@emotion/react';
15-
import type {
16-
CustomCellRenderer,
17-
CustomGridColumnProps,
18-
CustomGridColumnsConfiguration,
19-
} from '@kbn/unified-data-table';
15+
import type { CustomCellRenderer, CustomGridColumnsConfiguration } from '@kbn/unified-data-table';
2016
import {
2117
DataLoadingState,
2218
UnifiedDataTable,
@@ -30,8 +26,9 @@ import { difference, intersection, isEqual } from 'lodash';
3026
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
3127
import { FormattedMessage } from '@kbn/i18n-react';
3228
import { memoize } from 'lodash';
29+
import { KBN_FIELD_TYPES } from '@kbn/field-types';
3330
import { RowColumnCreator } from './row_column_creator';
34-
import { getColumnInputRenderer } from './grid_custom_renderers/column_input_renderer';
31+
import { getColumnHeaderRenderer } from './grid_custom_renderers/column_header_renderer';
3532
import { type KibanaContextExtra } from '../types';
3633
import { getCellValueRenderer } from './grid_custom_renderers/cell_value_renderer';
3734
import { getValueInputPopover } from './grid_custom_renderers/value_input_popover';
@@ -169,24 +166,24 @@ const DataGrid: React.FC<ESQLDataGridProps> = (props) => {
169166
const customGridColumnsConfiguration = useMemo<CustomGridColumnsConfiguration>(() => {
170167
return renderedColumns.reduce<CustomGridColumnsConfiguration>(
171168
(acc, columnName, columnIndex) => {
172-
if (!props.dataView.fields.getByName(columnName)) {
173-
const editMode = editingColumnIndex === columnIndex;
174-
acc[columnName] = memoize(
175-
getColumnInputRenderer(
176-
columnName,
177-
columnIndex,
178-
editMode,
179-
setEditingColumnIndex,
180-
indexUpdateService,
181-
indexEditorTelemetryService
182-
)
183-
);
184-
} else {
185-
acc[columnName] = (customGridColumnProps: CustomGridColumnProps) => ({
186-
...customGridColumnProps.column,
187-
actions: { showHide: false, showSortAsc: false, showSortDesc: false },
188-
});
189-
}
169+
const isSavedColumn = !!props.dataView.fields.getByName(columnName);
170+
const editMode = editingColumnIndex === columnIndex;
171+
const columnType = columnsMeta[columnName]?.esType;
172+
const isUnsupportedESQLType = columnsMeta[columnName]?.type === KBN_FIELD_TYPES.UNKNOWN;
173+
acc[columnName] = memoize(
174+
getColumnHeaderRenderer(
175+
columnName,
176+
columnType,
177+
columnIndex,
178+
isSavedColumn,
179+
isUnsupportedESQLType,
180+
editMode,
181+
setEditingColumnIndex,
182+
indexUpdateService,
183+
indexEditorTelemetryService
184+
)
185+
);
186+
190187
return acc;
191188
},
192189
{} as CustomGridColumnsConfiguration
@@ -195,6 +192,7 @@ const DataGrid: React.FC<ESQLDataGridProps> = (props) => {
195192
renderedColumns,
196193
props.dataView.fields,
197194
editingColumnIndex,
195+
columnsMeta,
198196
indexUpdateService,
199197
indexEditorTelemetryService,
200198
]);
@@ -253,7 +251,7 @@ const DataGrid: React.FC<ESQLDataGridProps> = (props) => {
253251
rowsPerPageState={rowsPerPage}
254252
rowsPerPageOptions={ROWS_PER_PAGE_OPTIONS}
255253
sampleSizeState={10000}
256-
canDragAndDropColumns
254+
canDragAndDropColumns={false}
257255
loadingState={isFetching ? DataLoadingState.loading : DataLoadingState.loaded}
258256
dataView={props.dataView}
259257
onSetColumns={setActiveColumns}

src/platform/packages/private/kbn-index-editor/src/components/grid_custom_renderers/cell_value_renderer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { type EuiDataGridRefProps } from '@kbn/unified-data-table';
1313
import { type DataGridCellValueElementProps } from '@kbn/unified-data-table';
1414
import type { DataTableRecord } from '@kbn/discover-utils';
1515
import { FormattedMessage } from '@kbn/i18n-react';
16+
import { getCellValue } from '../../utils';
1617

1718
export const getCellValueRenderer =
1819
(
@@ -22,8 +23,7 @@ export const getCellValueRenderer =
2223
): FunctionComponent<DataGridCellValueElementProps> =>
2324
({ rowIndex, colIndex, columnId }) => {
2425
const row = rows[rowIndex];
25-
26-
const cellValue = row.flattened[columnId]?.toString();
26+
const cellValue = getCellValue(row.flattened[columnId]);
2727

2828
const onEditStartHandler = () => {
2929
dataTableRef.current?.openCellPopover({
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import {
11+
useEuiTheme,
12+
findElementBySelectorOrRef,
13+
EuiButtonEmpty,
14+
EuiPopover,
15+
EuiForm,
16+
EuiFormRow,
17+
EuiPopoverFooter,
18+
EuiFieldText,
19+
EuiFlexGroup,
20+
EuiButton,
21+
EuiText,
22+
EuiIconTip,
23+
} from '@elastic/eui';
24+
import { i18n } from '@kbn/i18n';
25+
import type { PropsWithChildren } from 'react';
26+
import React, { useCallback, useMemo } from 'react';
27+
import { FormattedMessage } from '@kbn/i18n-react';
28+
import { FieldSelect } from '@kbn/field-utils';
29+
import { useKibana } from '@kbn/kibana-react-plugin/public';
30+
import { useAddColumnName, errorMessages } from '../../hooks/use_add_column_name';
31+
import type { IndexEditorTelemetryService } from '../../telemetry/telemetry_service';
32+
import { isPlaceholderColumn } from '../../utils';
33+
import type { KibanaContextExtra } from '../../types';
34+
35+
interface ColumnHeaderPopoverProps {
36+
isColumnInEditMode: boolean;
37+
setEditingColumnIndex: (columnIndex: number | null) => void;
38+
isSavedColumn: boolean;
39+
isUnsupportedESQLType: boolean;
40+
initialColumnName: string;
41+
initialColumnType: string | undefined;
42+
columnIndex: number;
43+
telemetryService: IndexEditorTelemetryService;
44+
originalColumnDisplay: React.ReactNode;
45+
}
46+
47+
export const COLUMN_INDEX_PROP = 'data-column-index';
48+
49+
export const ColumnHeaderPopover = ({
50+
isColumnInEditMode,
51+
setEditingColumnIndex,
52+
isSavedColumn,
53+
initialColumnName,
54+
initialColumnType,
55+
isUnsupportedESQLType,
56+
columnIndex,
57+
telemetryService,
58+
originalColumnDisplay,
59+
}: PropsWithChildren<ColumnHeaderPopoverProps>) => {
60+
const { euiTheme } = useEuiTheme();
61+
62+
const {
63+
services: { docLinks },
64+
} = useKibana<KibanaContextExtra>();
65+
66+
const { columnType, setColumnType, columnName, setColumnName, saveColumn, validationError } =
67+
useAddColumnName(initialColumnName, initialColumnType);
68+
69+
const canSubmit = useMemo(
70+
() => columnType && columnName.length > 0 && !validationError,
71+
[columnType, columnName, validationError]
72+
);
73+
74+
const onSubmit = useCallback(
75+
(event: React.FormEvent<HTMLFormElement>) => {
76+
event.preventDefault();
77+
event.stopPropagation();
78+
79+
if (columnName && !validationError) {
80+
setEditingColumnIndex(null);
81+
saveColumn();
82+
} else {
83+
telemetryService.trackEditInteraction({
84+
actionType: 'add_column',
85+
failureReason: validationError || 'EMPTY_NAME',
86+
});
87+
}
88+
},
89+
[columnName, validationError, setEditingColumnIndex, saveColumn, telemetryService]
90+
);
91+
92+
const columnLabel = isPlaceholderColumn(initialColumnName) ? (
93+
<FormattedMessage
94+
id="indexEditor.flyout.grid.columnHeader.add"
95+
defaultMessage="Add a column…"
96+
/>
97+
) : (
98+
// The default column header display comming from UnifiedDataTable, the type icon + column name
99+
<EuiFlexGroup alignItems="center" gutterSize="s" wrap={false} css={{ cursor: 'pointer' }}>
100+
{isUnsupportedESQLType && (
101+
<EuiIconTip
102+
type="warning"
103+
color="warning"
104+
size="m"
105+
content={i18n.translate('indexEditor.columnHeader.unsupportedWarning', {
106+
defaultMessage: `ES|QL doesn't support the {unsupportedType} data type yet. You can still set columns of this index to this type and save them, but Discover won't display them and they will be hidden from this view if you open it again later.`,
107+
values: { unsupportedType: initialColumnType },
108+
})}
109+
className="fieldWarningTip"
110+
anchorProps={{
111+
css: { display: 'flex', marginLeft: euiTheme.size.xxs },
112+
}}
113+
/>
114+
)}
115+
{originalColumnDisplay}
116+
</EuiFlexGroup>
117+
);
118+
119+
const errorMessage = useMemo(() => {
120+
if (!validationError) return;
121+
return errorMessages[validationError]
122+
? errorMessages[validationError](columnName)
123+
: validationError;
124+
}, [validationError, columnName]);
125+
126+
const returnFocus = useCallback(() => {
127+
requestAnimationFrame(() => {
128+
const headerWrapper = findElementBySelectorOrRef(`[${COLUMN_INDEX_PROP}="${columnIndex}"]`);
129+
130+
if (headerWrapper) {
131+
headerWrapper.focus();
132+
}
133+
});
134+
135+
return false;
136+
}, [columnIndex]);
137+
138+
if (isSavedColumn) {
139+
return columnLabel;
140+
}
141+
142+
const triggerButton = (
143+
// This button is keyboard accesible via the column actions menu.
144+
// eslint-disable-next-line @elastic/eui/accessible-interactive-element
145+
<EuiButtonEmpty
146+
data-test-subj="indexEditorColumnNameButton"
147+
aria-label={i18n.translate('indexEditor.columnHeaderEdit.ariaLabel', {
148+
defaultMessage: 'Edit column',
149+
})}
150+
css={{
151+
color: euiTheme.colors.textSubdued,
152+
width: '100%',
153+
height: euiTheme.size.xl,
154+
}}
155+
tabIndex={-1}
156+
flush="left"
157+
contentProps={{
158+
css: {
159+
justifyContent: 'left',
160+
},
161+
}}
162+
onClick={() => setEditingColumnIndex(columnIndex)}
163+
>
164+
{columnLabel}
165+
</EuiButtonEmpty>
166+
);
167+
168+
return (
169+
<EuiPopover
170+
button={triggerButton}
171+
isOpen={isColumnInEditMode}
172+
closePopover={() => setEditingColumnIndex(null)}
173+
focusTrapProps={{
174+
noIsolation: false,
175+
clickOutsideDisables: false,
176+
onClickOutside: (e) => {
177+
// This prevents closing the popover when clicking on the EuiSelect options
178+
if (e.isTrusted) {
179+
setEditingColumnIndex(null);
180+
}
181+
},
182+
returnFocus,
183+
}}
184+
hasArrow={false}
185+
onKeyDown={(e: React.KeyboardEvent) => {
186+
// This prevents focus for going back to the grid header.
187+
if (e.key === 'Enter') {
188+
e.stopPropagation();
189+
}
190+
}}
191+
>
192+
<EuiForm component="form" onSubmit={onSubmit} css={{ width: 300 }}>
193+
<EuiFormRow
194+
label={i18n.translate('indexEditor.columnHeaderEdit.fieldType', {
195+
defaultMessage: 'Select a field type',
196+
})}
197+
helpText={i18n.translate('indexEditor.columnHeaderEdit.fieldTypeHelpText', {
198+
defaultMessage: `You won't be able to change the type after saving the lookup index.`,
199+
})}
200+
>
201+
<FieldSelect
202+
selectedType={columnType || null}
203+
onTypeChange={setColumnType}
204+
data-test-subj="indexEditorColumnTypeSelect"
205+
docLinks={docLinks}
206+
/>
207+
</EuiFormRow>
208+
{(columnType || columnName.length > 0) && (
209+
<EuiPopoverFooter>
210+
<EuiFormRow
211+
label={i18n.translate('indexEditor.columnHeaderEdit.columnNameLabel', {
212+
defaultMessage: 'Name',
213+
})}
214+
isInvalid={Boolean(errorMessage)}
215+
error={errorMessage}
216+
>
217+
<EuiFieldText
218+
isInvalid={Boolean(errorMessage)}
219+
data-test-subj="indexEditorColumnNameInput"
220+
value={columnName}
221+
placeholder={i18n.translate('indexEditor.columnHeaderEdit.columnNamePlaceholder', {
222+
defaultMessage: 'Enter field name',
223+
})}
224+
fullWidth
225+
controlOnly
226+
compressed
227+
onChange={(e) => {
228+
setColumnName(e.target.value);
229+
}}
230+
/>
231+
</EuiFormRow>
232+
<EuiFlexGroup
233+
justifyContent="flexEnd"
234+
gutterSize="m"
235+
css={{ marginTop: euiTheme.size.l }}
236+
>
237+
<EuiButton color="text" size="s" onClick={() => setEditingColumnIndex(null)}>
238+
<EuiText size="xs">
239+
<FormattedMessage
240+
id="indexEditor.flyout.grid.columnHeader.cancelButton"
241+
defaultMessage="Cancel"
242+
/>
243+
</EuiText>
244+
</EuiButton>
245+
<EuiButton
246+
data-test-subj="indexEditorColumnNameAcceptButton"
247+
fill
248+
type="submit"
249+
disabled={!canSubmit}
250+
size="s"
251+
>
252+
<EuiText size="xs">
253+
<FormattedMessage
254+
id="indexEditor.flyout.grid.columnHeader.acceptButton"
255+
defaultMessage="Accept"
256+
/>
257+
</EuiText>
258+
</EuiButton>
259+
</EuiFlexGroup>
260+
</EuiPopoverFooter>
261+
)}
262+
</EuiForm>
263+
</EuiPopover>
264+
);
265+
};

0 commit comments

Comments
 (0)