[Logs UI] Make column configurations reorderable (#41035) (#41697)

* [Logs UI] Make column configurations reorderable

* Improve typing aand memoize callback

* Guard against index bounds and rename reorderLogColumns

* Fix useCallback memoization

* Add functional test for reordering log columns

* Use browser.keys instead of Key in functional test
This commit is contained in:
Zacqary Adam Xeper 2019-07-23 09:56:51 -05:00 committed by GitHub
parent 8148b2dd26
commit 2f31e58ee8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 165 additions and 50 deletions

View file

@ -205,6 +205,18 @@ export class WebElementWrapper {
});
}
/**
* Focuses this element.
*
* @return {Promise<void>}
*/
public async focus() {
await this.retryCall(async function focus(wrapper) {
await wrapper.scrollIntoViewIfNecessary();
await wrapper.driver.executeScript(`arguments[0].focus()`, wrapper._webElement);
});
}
/**
* Clear the value of this element. This command has no effect if the underlying DOM element
* is neither a text INPUT element nor a TEXTAREA element.

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiDraggable, EuiDragDropContext } from '@elastic/eui';
type PropsOf<T> = T extends React.ComponentType<infer ComponentProps> ? ComponentProps : never;
type FirstArgumentOf<Func> = Func extends ((arg1: infer FirstArgument, ...rest: any[]) => any)
? FirstArgument
: never;
export type DragHandleProps = FirstArgumentOf<
Exclude<PropsOf<typeof EuiDraggable>['children'], React.ReactElement>
>['dragHandleProps'];
export type DropResult = FirstArgumentOf<FirstArgumentOf<typeof EuiDragDropContext>['onDragEnd']>;

View file

@ -106,6 +106,20 @@ export const useLogColumnsConfigurationFormState = ({
[formState.logColumns]
);
const moveLogColumn = useCallback(
(sourceIndex, destinationIndex) => {
if (destinationIndex >= 0 && sourceIndex < formState.logColumns.length - 1) {
const newLogColumns = [...formState.logColumns];
newLogColumns.splice(destinationIndex, 0, newLogColumns.splice(sourceIndex, 1)[0]);
setFormStateChanges(changes => ({
...changes,
logColumns: newLogColumns,
}));
}
},
[formState.logColumns]
);
const errors = useMemo(
() =>
logColumnConfigurationProps.length <= 0
@ -125,6 +139,7 @@ export const useLogColumnsConfigurationFormState = ({
return {
addLogColumn,
moveLogColumn,
errors,
logColumnConfigurationProps,
formState,

View file

@ -14,9 +14,14 @@ import {
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
EuiDragDropContext,
EuiDraggable,
EuiDroppable,
EuiIcon,
} from '@elastic/eui';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import React, { useCallback } from 'react';
import { DragHandleProps, DropResult } from '../../../../../common/eui_draggable';
import { AddLogColumnButtonAndPopover } from './add_log_column_popover';
import {
@ -30,70 +35,95 @@ interface LogColumnsConfigurationPanelProps {
isLoading: boolean;
logColumnConfiguration: LogColumnConfigurationProps[];
addLogColumn: (logColumn: LogColumnConfiguration) => void;
moveLogColumn: (sourceIndex: number, destinationIndex: number) => void;
}
export const LogColumnsConfigurationPanel: React.FunctionComponent<
LogColumnsConfigurationPanelProps
> = ({ addLogColumn, availableFields, isLoading, logColumnConfiguration }) => (
<EuiForm>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="s" data-test-subj="sourceConfigurationLogColumnsSectionTitle">
<h3>
<FormattedMessage
id="xpack.infra.sourceConfiguration.logColumnsSectionTitle"
defaultMessage="Columns"
/>
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AddLogColumnButtonAndPopover
addLogColumn={addLogColumn}
availableFields={availableFields}
isDisabled={isLoading}
/>
</EuiFlexItem>
</EuiFlexGroup>
{logColumnConfiguration.length > 0 ? (
logColumnConfiguration.map((column, index) => (
<LogColumnConfigurationPanel
logColumnConfigurationProps={column}
key={`logColumnConfigurationPanel-${index}`}
/>
))
) : (
<LogColumnConfigurationEmptyPrompt />
)}
</EuiForm>
);
> = ({ addLogColumn, moveLogColumn, availableFields, isLoading, logColumnConfiguration }) => {
const onDragEnd = useCallback(
({ source, destination }: DropResult) =>
destination && moveLogColumn(source.index, destination.index),
[moveLogColumn]
);
return (
<EuiForm>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="s" data-test-subj="sourceConfigurationLogColumnsSectionTitle">
<h3>
<FormattedMessage
id="xpack.infra.sourceConfiguration.logColumnsSectionTitle"
defaultMessage="Columns"
/>
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AddLogColumnButtonAndPopover
addLogColumn={addLogColumn}
availableFields={availableFields}
isDisabled={isLoading}
/>
</EuiFlexItem>
</EuiFlexGroup>
{logColumnConfiguration.length > 0 ? (
<EuiDragDropContext onDragEnd={onDragEnd}>
<EuiDroppable droppableId="COLUMN_CONFIG_DROPPABLE_AREA">
<>
{/* Fragment here necessary for typechecking */}
{logColumnConfiguration.map((column, index) => (
<EuiDraggable
key={`logColumnConfigurationPanel-${column.logColumnConfiguration.id}`}
index={index}
draggableId={column.logColumnConfiguration.id}
customDragHandle
>
{provided => (
<LogColumnConfigurationPanel
dragHandleProps={provided.dragHandleProps}
logColumnConfigurationProps={column}
/>
)}
</EuiDraggable>
))}
</>
</EuiDroppable>
</EuiDragDropContext>
) : (
<LogColumnConfigurationEmptyPrompt />
)}
</EuiForm>
);
};
interface LogColumnConfigurationPanelProps {
logColumnConfigurationProps: LogColumnConfigurationProps;
dragHandleProps: DragHandleProps;
}
const LogColumnConfigurationPanel: React.FunctionComponent<LogColumnConfigurationPanelProps> = ({
logColumnConfigurationProps,
}) => (
const LogColumnConfigurationPanel: React.FunctionComponent<
LogColumnConfigurationPanelProps
> = props => (
<>
<EuiSpacer size="m" />
{logColumnConfigurationProps.type === 'timestamp' ? (
<TimestampLogColumnConfigurationPanel
logColumnConfigurationProps={logColumnConfigurationProps}
/>
) : logColumnConfigurationProps.type === 'message' ? (
<MessageLogColumnConfigurationPanel
logColumnConfigurationProps={logColumnConfigurationProps}
/>
{props.logColumnConfigurationProps.type === 'timestamp' ? (
<TimestampLogColumnConfigurationPanel {...props} />
) : props.logColumnConfigurationProps.type === 'message' ? (
<MessageLogColumnConfigurationPanel {...props} />
) : (
<FieldLogColumnConfigurationPanel logColumnConfigurationProps={logColumnConfigurationProps} />
<FieldLogColumnConfigurationPanel
logColumnConfigurationProps={props.logColumnConfigurationProps}
dragHandleProps={props.dragHandleProps}
/>
)}
</>
);
const TimestampLogColumnConfigurationPanel: React.FunctionComponent<
LogColumnConfigurationPanelProps
> = ({ logColumnConfigurationProps }) => (
> = ({ logColumnConfigurationProps, dragHandleProps }) => (
<ExplainedLogColumnConfigurationPanel
fieldName="Timestamp"
helpText={
@ -107,12 +137,13 @@ const TimestampLogColumnConfigurationPanel: React.FunctionComponent<
/>
}
removeColumn={logColumnConfigurationProps.remove}
dragHandleProps={dragHandleProps}
/>
);
const MessageLogColumnConfigurationPanel: React.FunctionComponent<
LogColumnConfigurationPanelProps
> = ({ logColumnConfigurationProps }) => (
> = ({ logColumnConfigurationProps, dragHandleProps }) => (
<ExplainedLogColumnConfigurationPanel
fieldName="Message"
helpText={
@ -123,19 +154,27 @@ const MessageLogColumnConfigurationPanel: React.FunctionComponent<
/>
}
removeColumn={logColumnConfigurationProps.remove}
dragHandleProps={dragHandleProps}
/>
);
const FieldLogColumnConfigurationPanel: React.FunctionComponent<{
logColumnConfigurationProps: FieldLogColumnConfigurationProps;
dragHandleProps: DragHandleProps;
}> = ({
logColumnConfigurationProps: {
logColumnConfiguration: { field },
remove,
},
dragHandleProps,
}) => (
<EuiPanel data-test-subj={`logColumnPanel fieldLogColumnPanel fieldLogColumnPanel:${field}`}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<div data-test-subj="moveLogColumnHandle" {...dragHandleProps}>
<EuiIcon type="grab" />
</div>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<FormattedMessage
id="xpack.infra.sourceConfiguration.fieldLogColumnTitle"
@ -156,11 +195,17 @@ const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{
fieldName: React.ReactNode;
helpText: React.ReactNode;
removeColumn: () => void;
}> = ({ fieldName, helpText, removeColumn }) => (
dragHandleProps: DragHandleProps;
}> = ({ fieldName, helpText, removeColumn, dragHandleProps }) => (
<EuiPanel
data-test-subj={`logColumnPanel systemLogColumnPanel systemLogColumnPanel:${fieldName}`}
>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<div data-test-subj="moveLogColumnHandle" {...dragHandleProps}>
<EuiIcon type="grab" />
</div>
</EuiFlexItem>
<EuiFlexItem grow={1}>{fieldName}</EuiFlexItem>
<EuiFlexItem grow={3}>
<EuiText size="s" color="subdued">

View file

@ -57,6 +57,7 @@ export const SourceConfigurationFlyout = injectI18n(
const {
addLogColumn,
moveLogColumn,
indicesConfigurationProps,
logColumnConfigurationProps,
errors,
@ -137,6 +138,7 @@ export const SourceConfigurationFlyout = injectI18n(
<EuiSpacer />
<LogColumnsConfigurationPanel
addLogColumn={addLogColumn}
moveLogColumn={moveLogColumn}
availableFields={availableFields}
isLoading={isLoading}
logColumnConfiguration={logColumnConfigurationProps}
@ -148,6 +150,7 @@ export const SourceConfigurationFlyout = injectI18n(
: [],
[
addLogColumn,
moveLogColumn,
availableFields,
indicesConfigurationProps,
intl.formatMessage,

View file

@ -105,6 +105,7 @@ export const useSourceConfigurationFormState = (configuration?: SourceConfigurat
return {
addLogColumn: logColumnsConfigurationFormState.addLogColumn,
moveLogColumn: logColumnsConfigurationFormState.moveLogColumn,
errors,
formState,
formStateChanges,

View file

@ -98,6 +98,8 @@ export default ({ getPageObjects, getService }: KibanaFunctionalTestDefaultProvi
await infraSourceConfigurationFlyout.addTimestampLogColumn();
await infraSourceConfigurationFlyout.addFieldLogColumn('host.name');
await infraSourceConfigurationFlyout.moveLogColumn(0, 1);
await infraSourceConfigurationFlyout.saveConfiguration();
await infraSourceConfigurationFlyout.closeFlyout();
});
@ -105,7 +107,7 @@ export default ({ getPageObjects, getService }: KibanaFunctionalTestDefaultProvi
it('renders the changed log columns with their headers', async () => {
const columnHeaderLabels = await infraLogStream.getColumnHeaderLabels();
expect(columnHeaderLabels).to.eql(['Timestamp', 'host.name', '']);
expect(columnHeaderLabels).to.eql(['host.name', 'Timestamp', '']);
const logStreamEntries = await infraLogStream.getStreamEntries();

View file

@ -13,6 +13,7 @@ export function InfraSourceConfigurationFlyoutProvider({
const find = getService('find');
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const browser = getService('browser');
return {
/**
@ -81,6 +82,25 @@ export function InfraSourceConfigurationFlyoutProvider({
await this.removeLogColumn(0);
}
},
async moveLogColumn(sourceIndex: number, destinationIndex: number) {
const logColumnPanel = (await this.getLogColumnPanels())[sourceIndex];
const moveLogColumnHandle = await testSubjects.findDescendant(
'moveLogColumnHandle',
logColumnPanel
);
await moveLogColumnHandle.focus();
const movementDifference = destinationIndex - sourceIndex;
await moveLogColumnHandle.pressKeys(browser.keys.SPACE);
for (let i = 0; i < Math.abs(movementDifference); i++) {
await new Promise(res => setTimeout(res, 100));
if (movementDifference > 0) {
await moveLogColumnHandle.pressKeys(browser.keys.ARROW_DOWN);
} else {
await moveLogColumnHandle.pressKeys(browser.keys.ARROW_UP);
}
}
await moveLogColumnHandle.pressKeys(browser.keys.SPACE);
},
/**
* Form and flyout