mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Add user permission check to CreateFieldButton Refetch data after creating field Add global styles to make Overlay z-index higher than timeline z-index Fix create runtime field loading state Update alert table columns after adding a new runtime field Updated documentation of 'overlays.openFlyout' public API Add cypress test Add CreateField button unit test # Conflicts: # docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md # src/core/public/public.api.md # x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
This commit is contained in:
parent
6b8a29532e
commit
0c3a8659da
27 changed files with 446 additions and 24 deletions
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) > [maskProps](./kibana-plugin-core-public.overlayflyoutopenoptions.maskprops.md)
|
||||
|
||||
## OverlayFlyoutOpenOptions.maskProps property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
maskProps?: EuiOverlayMaskProps;
|
||||
```
|
|
@ -20,6 +20,7 @@ export interface OverlayFlyoutOpenOptions
|
|||
| [className?](./kibana-plugin-core-public.overlayflyoutopenoptions.classname.md) | string | <i>(Optional)</i> |
|
||||
| [closeButtonAriaLabel?](./kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md) | string | <i>(Optional)</i> |
|
||||
| [hideCloseButton?](./kibana-plugin-core-public.overlayflyoutopenoptions.hideclosebutton.md) | boolean | <i>(Optional)</i> |
|
||||
| [maskProps](./kibana-plugin-core-public.overlayflyoutopenoptions.maskprops.md) | <code>EuiOverlayMaskProps</code> | |
|
||||
| [maxWidth?](./kibana-plugin-core-public.overlayflyoutopenoptions.maxwidth.md) | boolean \| number \| string | <i>(Optional)</i> |
|
||||
| [onClose?](./kibana-plugin-core-public.overlayflyoutopenoptions.onclose.md) | (flyout: OverlayRef) => void | <i>(Optional)</i> EuiFlyout onClose handler. If provided the consumer is responsible for calling flyout.close() to close the flyout; |
|
||||
| [ownFocus?](./kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md) | boolean | <i>(Optional)</i> |
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { EuiFlyout, EuiFlyoutSize } from '@elastic/eui';
|
||||
import { EuiFlyout, EuiFlyoutSize, EuiOverlayMaskProps } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Subject } from 'rxjs';
|
||||
|
@ -86,6 +86,7 @@ export interface OverlayFlyoutOpenOptions {
|
|||
size?: EuiFlyoutSize;
|
||||
maxWidth?: boolean | number | string;
|
||||
hideCloseButton?: boolean;
|
||||
maskProps?: EuiOverlayMaskProps;
|
||||
/**
|
||||
* EuiFlyout onClose handler.
|
||||
* If provided the consumer is responsible for calling flyout.close() to close the flyout;
|
||||
|
|
|
@ -18,6 +18,7 @@ import { EuiButtonEmptyProps } from '@elastic/eui';
|
|||
import { EuiConfirmModalProps } from '@elastic/eui';
|
||||
import { EuiFlyoutSize } from '@elastic/eui';
|
||||
import { EuiGlobalToastListToast } from '@elastic/eui';
|
||||
import { EuiOverlayMaskProps } from '@elastic/eui';
|
||||
import { History as History_2 } from 'history';
|
||||
import { Href } from 'history';
|
||||
import { IconType } from '@elastic/eui';
|
||||
|
@ -1063,6 +1064,8 @@ export interface OverlayFlyoutOpenOptions {
|
|||
// (undocumented)
|
||||
hideCloseButton?: boolean;
|
||||
// (undocumented)
|
||||
maskProps?: EuiOverlayMaskProps;
|
||||
// (undocumented)
|
||||
maxWidth?: boolean | number | string;
|
||||
onClose?: (flyout: OverlayRef) => void;
|
||||
// (undocumented)
|
||||
|
|
|
@ -150,6 +150,9 @@ export const getFieldEditorOpener =
|
|||
flyout.close();
|
||||
}
|
||||
},
|
||||
maskProps: {
|
||||
className: 'indexPatternFieldEditorMaskOverlay',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { cleanKibana } from '../../tasks/common';
|
||||
|
||||
import { loginAndWaitForPage } from '../../tasks/login';
|
||||
import { openTimelineUsingToggle } from '../../tasks/security_main';
|
||||
import { openTimelineFieldsBrowser, populateTimeline } from '../../tasks/timeline';
|
||||
|
||||
import { HOSTS_URL, ALERTS_URL } from '../../urls/navigation';
|
||||
|
||||
import { waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded } from '../../tasks/alerts';
|
||||
import { createCustomRuleActivated } from '../../tasks/api_calls/rules';
|
||||
|
||||
import { getNewRule } from '../../objects/rule';
|
||||
import { refreshPage } from '../../tasks/security_header';
|
||||
import { waitForAlertsToPopulate } from '../../tasks/create_new_rule';
|
||||
import { openEventsViewerFieldsBrowser } from '../../tasks/hosts/events';
|
||||
|
||||
describe('Create DataView runtime field', () => {
|
||||
before(() => {
|
||||
cleanKibana();
|
||||
});
|
||||
|
||||
it('adds field to alert table', () => {
|
||||
const fieldName = 'field.name.alert.page';
|
||||
loginAndWaitForPage(ALERTS_URL);
|
||||
waitForAlertsPanelToBeLoaded();
|
||||
waitForAlertsIndexToBeCreated();
|
||||
createCustomRuleActivated(getNewRule());
|
||||
refreshPage();
|
||||
waitForAlertsToPopulate(500);
|
||||
openEventsViewerFieldsBrowser();
|
||||
|
||||
cy.get('[data-test-subj="create-field"]').click();
|
||||
cy.get('.indexPatternFieldEditorMaskOverlay').find('[data-test-subj="input"]').type(fieldName);
|
||||
cy.get('[data-test-subj="fieldSaveButton"]').click();
|
||||
|
||||
cy.get(
|
||||
`[data-test-subj="events-viewer-panel"] [data-test-subj="dataGridHeaderCell-${fieldName}"]`
|
||||
).should('exist');
|
||||
});
|
||||
|
||||
it('adds field to timeline', () => {
|
||||
const fieldName = 'field.name.timeline';
|
||||
|
||||
loginAndWaitForPage(HOSTS_URL);
|
||||
openTimelineUsingToggle();
|
||||
populateTimeline();
|
||||
openTimelineFieldsBrowser();
|
||||
|
||||
cy.get('[data-test-subj="create-field"]').click();
|
||||
cy.get('.indexPatternFieldEditorMaskOverlay').find('[data-test-subj="input"]').type(fieldName);
|
||||
cy.get('[data-test-subj="fieldSaveButton"]').click();
|
||||
|
||||
cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${fieldName}"]`).should(
|
||||
'exist'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -38,7 +38,8 @@
|
|||
"lens",
|
||||
"lists",
|
||||
"home",
|
||||
"telemetry"
|
||||
"telemetry",
|
||||
"indexPatternFieldEditor"
|
||||
],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
|
|
|
@ -10,6 +10,7 @@ import { connect, ConnectedProps, useDispatch } from 'react-redux';
|
|||
import deepEqual from 'fast-deep-equal';
|
||||
import styled from 'styled-components';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
|
||||
import { inputsModel, inputsSelectors, State } from '../../store';
|
||||
import { inputsActions } from '../../store/actions';
|
||||
import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline';
|
||||
|
@ -27,6 +28,7 @@ import { DetailsPanel } from '../../../timelines/components/side_panel';
|
|||
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
|
||||
import { useCreateFieldButton } from '../../../timelines/components/create_field_button';
|
||||
|
||||
const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = [];
|
||||
|
||||
|
@ -164,6 +166,8 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
}, [id, timelineQuery, globalQuery]);
|
||||
const bulkActions = useMemo(() => ({ onAlertStatusActionSuccess }), [onAlertStatusActionSuccess]);
|
||||
|
||||
const createFieldComponent = useCreateFieldButton(scopeId, id);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FullScreenContainer $isFullScreen={globalFullScreen}>
|
||||
|
@ -206,6 +210,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
trailingControlColumns,
|
||||
type: 'embedded',
|
||||
unit,
|
||||
createFieldComponent,
|
||||
})}
|
||||
</InspectButtonContainer>
|
||||
</FullScreenContainer>
|
||||
|
|
|
@ -31,7 +31,7 @@ jest.mock('react-redux', () => {
|
|||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
jest.mock('../../lib/kibana'); // , () => ({
|
||||
jest.mock('../../lib/kibana');
|
||||
|
||||
describe('source/index.tsx', () => {
|
||||
describe('getBrowserFields', () => {
|
||||
|
@ -40,11 +40,11 @@ describe('source/index.tsx', () => {
|
|||
expect(fields).toEqual({});
|
||||
});
|
||||
|
||||
test('it returns the same input with the same title', () => {
|
||||
getBrowserFields('title 1', []);
|
||||
// Since it is memoized it will return the same output which is empty object given 'title 1' a second time
|
||||
const fields = getBrowserFields('title 1', mocksSource.indexFields as IndexField[]);
|
||||
expect(fields).toEqual({});
|
||||
test('it returns the same input given the same title and same fields length', () => {
|
||||
const oldFields = getBrowserFields('title 1', mocksSource.indexFields as IndexField[]);
|
||||
const newFields = getBrowserFields('title 1', mocksSource.indexFields as IndexField[]);
|
||||
// Since it is memoized it will return the same object instance
|
||||
expect(newFields).toBe(oldFields);
|
||||
});
|
||||
|
||||
test('it transforms input into output as expected', () => {
|
||||
|
|
|
@ -78,8 +78,7 @@ export const getBrowserFields = memoizeOne(
|
|||
return accumulator;
|
||||
}, {});
|
||||
},
|
||||
// Update the value only if _title has changed
|
||||
(newArgs, lastArgs) => newArgs[0] === lastArgs[0]
|
||||
(newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1].length === lastArgs[1].length
|
||||
);
|
||||
|
||||
export const getDocValueFields = memoizeOne(
|
||||
|
@ -97,8 +96,7 @@ export const getDocValueFields = memoizeOne(
|
|||
return accumulator;
|
||||
}, [])
|
||||
: [],
|
||||
// Update the value only if _title has changed
|
||||
(newArgs, lastArgs) => newArgs[0] === lastArgs[0]
|
||||
(newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1].length === lastArgs[1].length
|
||||
);
|
||||
|
||||
export const indicesExistOrDataTemporarilyUnavailable = (
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { render, fireEvent, act, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { CreateFieldButton } from './index';
|
||||
import {
|
||||
indexPatternFieldEditorPluginMock,
|
||||
Start,
|
||||
} from '../../../../../../../src/plugins/index_pattern_field_editor/public/mocks';
|
||||
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { DataView } from '../../../../../../../src/plugins/data/common';
|
||||
import { TimelineId } from '../../../../common';
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
||||
let mockIndexPatternFieldEditor: Start;
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
const runAllPromises = () => new Promise(setImmediate);
|
||||
|
||||
describe('CreateFieldButton', () => {
|
||||
beforeEach(() => {
|
||||
mockIndexPatternFieldEditor = indexPatternFieldEditorPluginMock.createStartContract();
|
||||
useKibanaMock().services.indexPatternFieldEditor = mockIndexPatternFieldEditor;
|
||||
useKibanaMock().services.data.dataViews.get = () => new Promise(() => undefined);
|
||||
});
|
||||
|
||||
it('displays the button when user has permissions', () => {
|
||||
mockIndexPatternFieldEditor.userPermissions.editIndexPattern = () => true;
|
||||
|
||||
render(
|
||||
<CreateFieldButton
|
||||
selectedDataViewId={'dataViewId'}
|
||||
onClick={() => undefined}
|
||||
timelineId={TimelineId.detectionsPage}
|
||||
/>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("doesn't display the button when user doesn't have permissions", () => {
|
||||
mockIndexPatternFieldEditor.userPermissions.editIndexPattern = () => false;
|
||||
render(
|
||||
<CreateFieldButton
|
||||
selectedDataViewId={'dataViewId'}
|
||||
onClick={() => undefined}
|
||||
timelineId={TimelineId.detectionsPage}
|
||||
/>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls 'onClick' param when the button is clicked", async () => {
|
||||
mockIndexPatternFieldEditor.userPermissions.editIndexPattern = () => true;
|
||||
useKibanaMock().services.data.dataViews.get = () => Promise.resolve({} as DataView);
|
||||
|
||||
const onClickParam = jest.fn();
|
||||
await act(async () => {
|
||||
render(
|
||||
<CreateFieldButton
|
||||
selectedDataViewId={'dataViewId'}
|
||||
onClick={onClickParam}
|
||||
timelineId={TimelineId.detectionsPage}
|
||||
/>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
await runAllPromises();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
expect(onClickParam).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IndexPattern, IndexPatternField } from '../../../../../../../src/plugins/data/public';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { CreateFieldComponentType, TimelineId } from '../../../../../timelines/common';
|
||||
import { tGridActions } from '../../../../../timelines/public';
|
||||
import { useDataView } from '../../../common/containers/source/use_data_view';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { sourcererSelectors } from '../../../common/store';
|
||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants';
|
||||
import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers';
|
||||
|
||||
interface CreateFieldButtonProps {
|
||||
selectedDataViewId: string;
|
||||
onClick: () => void;
|
||||
timelineId: TimelineId;
|
||||
}
|
||||
const StyledButton = styled(EuiButton)`
|
||||
margin-left: ${({ theme }) => theme.eui.paddingSizes.m};
|
||||
`;
|
||||
|
||||
export const CreateFieldButton = React.memo<CreateFieldButtonProps>(
|
||||
({ selectedDataViewId, onClick: onClickParam, timelineId }) => {
|
||||
const [dataView, setDataView] = useState<IndexPattern | null>(null);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { indexFieldsSearch } = useDataView();
|
||||
const {
|
||||
indexPatternFieldEditor,
|
||||
data: { dataViews },
|
||||
} = useKibana().services;
|
||||
|
||||
useEffect(() => {
|
||||
dataViews.get(selectedDataViewId).then((dataViewResponse) => {
|
||||
setDataView(dataViewResponse);
|
||||
});
|
||||
}, [selectedDataViewId, dataViews]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (dataView) {
|
||||
indexPatternFieldEditor?.openEditor({
|
||||
ctx: { indexPattern: dataView },
|
||||
onSave: (field: IndexPatternField) => {
|
||||
// Fetch the updated list of fields
|
||||
indexFieldsSearch(selectedDataViewId);
|
||||
|
||||
// Add the new field to the event table
|
||||
dispatch(
|
||||
tGridActions.upsertColumn({
|
||||
column: {
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: field.name,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
id: timelineId,
|
||||
index: 0,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
onClickParam();
|
||||
}, [
|
||||
indexPatternFieldEditor,
|
||||
dataView,
|
||||
onClickParam,
|
||||
indexFieldsSearch,
|
||||
selectedDataViewId,
|
||||
dispatch,
|
||||
timelineId,
|
||||
]);
|
||||
|
||||
if (!indexPatternFieldEditor?.userPermissions.editIndexPattern()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledButton
|
||||
iconType={dataView ? 'plusInCircle' : 'none'}
|
||||
aria-label={i18n.CREATE_FIELD}
|
||||
data-test-subj="create-field"
|
||||
onClick={onClick}
|
||||
isLoading={!dataView}
|
||||
>
|
||||
{i18n.CREATE_FIELD}
|
||||
</StyledButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CreateFieldButton.displayName = 'CreateFieldButton';
|
||||
|
||||
/**
|
||||
*
|
||||
* Returns a memoised 'CreateFieldButton' with only an 'onClick' property.
|
||||
*/
|
||||
export const useCreateFieldButton = (
|
||||
sourcererScope: SourcererScopeName,
|
||||
timelineId: TimelineId
|
||||
) => {
|
||||
const scopeIdSelector = useMemo(() => sourcererSelectors.scopeIdSelector(), []);
|
||||
const { selectedDataViewId } = useDeepEqualSelector((state) =>
|
||||
scopeIdSelector(state, sourcererScope)
|
||||
);
|
||||
|
||||
const createFieldComponent = useMemo(() => {
|
||||
// It receives onClick props from field browser in order to close the modal.
|
||||
const CreateFieldButtonComponent: CreateFieldComponentType = ({ onClick }) => (
|
||||
<CreateFieldButton
|
||||
selectedDataViewId={selectedDataViewId}
|
||||
onClick={onClick}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
);
|
||||
|
||||
return CreateFieldButtonComponent;
|
||||
}, [selectedDataViewId, timelineId]);
|
||||
|
||||
return createFieldComponent;
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const CREATE_FIELD = i18n.translate(
|
||||
'xpack.securitySolution.fieldBrowser.createFieldButton',
|
||||
{
|
||||
defaultMessage: 'Create field',
|
||||
}
|
||||
);
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { EuiFlyout, EuiFlyoutProps } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import styled, { createGlobalStyle } from 'styled-components';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { StatefulTimeline } from '../../timeline';
|
||||
|
@ -29,6 +29,17 @@ const StyledEuiFlyout = styled(EuiFlyout)<EuiFlyoutProps>`
|
|||
z-index: ${({ theme }) => theme.eui.euiZLevel4};
|
||||
`;
|
||||
|
||||
// SIDE EFFECT: the following creates a global class selector
|
||||
const IndexPatternFieldEditorOverlayGlobalStyle = createGlobalStyle<{
|
||||
theme: { eui: { euiZLevel5: number } };
|
||||
}>`
|
||||
.indexPatternFieldEditorMaskOverlay {
|
||||
${({ theme }) => `
|
||||
z-index: ${theme.eui.euiZLevel5};
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({
|
||||
timelineId,
|
||||
visible = true,
|
||||
|
@ -51,6 +62,7 @@ const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({
|
|||
ownFocus={false}
|
||||
style={{ visibility: visible ? 'visible' : 'hidden' }}
|
||||
>
|
||||
<IndexPatternFieldEditorOverlayGlobalStyle />
|
||||
<StatefulTimeline
|
||||
renderCellValue={DefaultCellRenderer}
|
||||
rowRenderers={defaultRowRenderers}
|
||||
|
|
|
@ -86,6 +86,7 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = ({
|
|||
sort,
|
||||
tabType,
|
||||
timelineId,
|
||||
createFieldComponent,
|
||||
}) => {
|
||||
const { timelines: timelinesUi } = useKibana().services;
|
||||
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
|
||||
|
@ -183,6 +184,7 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = ({
|
|||
browserFields,
|
||||
columnHeaders,
|
||||
timelineId,
|
||||
createFieldComponent,
|
||||
})}
|
||||
</FieldBrowserContainer>
|
||||
</EventsTh>
|
||||
|
|
|
@ -33,6 +33,9 @@ import {
|
|||
import { Sort } from '../sort';
|
||||
import { ColumnHeader } from './column_header';
|
||||
|
||||
import { SourcererScopeName } from '../../../../../common/store/sourcerer/model';
|
||||
import { useCreateFieldButton } from '../../../create_field_button';
|
||||
|
||||
interface Props {
|
||||
actionsColumnWidth: number;
|
||||
browserFields: BrowserFields;
|
||||
|
@ -169,6 +172,11 @@ export const ColumnHeadersComponent = ({
|
|||
[trailingControlColumns]
|
||||
);
|
||||
|
||||
const createFieldComponent = useCreateFieldButton(
|
||||
SourcererScopeName.timeline,
|
||||
timelineId as TimelineId
|
||||
);
|
||||
|
||||
const LeadingHeaderActions = useMemo(() => {
|
||||
return leadingHeaderCells.map(
|
||||
(Header: React.ComponentType<HeaderActionProps> | React.ComponentType | undefined, index) => {
|
||||
|
@ -194,6 +202,7 @@ export const ColumnHeadersComponent = ({
|
|||
sort={sort}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
createFieldComponent={createFieldComponent}
|
||||
/>
|
||||
)}
|
||||
</EventsThGroupActions>
|
||||
|
@ -206,6 +215,7 @@ export const ColumnHeadersComponent = ({
|
|||
actionsColumnWidth,
|
||||
browserFields,
|
||||
columnHeaders,
|
||||
createFieldComponent,
|
||||
isEventViewer,
|
||||
isSelectAllChecked,
|
||||
onSelectAll,
|
||||
|
@ -241,6 +251,7 @@ export const ColumnHeadersComponent = ({
|
|||
sort={sort}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
createFieldComponent={createFieldComponent}
|
||||
/>
|
||||
)}
|
||||
</EventsThGroupActions>
|
||||
|
@ -253,6 +264,7 @@ export const ColumnHeadersComponent = ({
|
|||
actionsColumnWidth,
|
||||
browserFields,
|
||||
columnHeaders,
|
||||
createFieldComponent,
|
||||
isEventViewer,
|
||||
isSelectAllChecked,
|
||||
onSelectAll,
|
||||
|
|
|
@ -27,7 +27,6 @@ import { defaultRowRenderers } from './renderers';
|
|||
|
||||
jest.mock('../../../../common/lib/kibana/hooks');
|
||||
jest.mock('../../../../common/hooks/use_app_toasts');
|
||||
|
||||
jest.mock('../../../../common/lib/kibana', () => {
|
||||
const originalModule = jest.requireActual('../../../../common/lib/kibana');
|
||||
return {
|
||||
|
|
|
@ -41,6 +41,7 @@ import { Management } from './management';
|
|||
import { Ueba } from './ueba';
|
||||
import { LicensingPluginStart, LicensingPluginSetup } from '../../licensing/public';
|
||||
import { DashboardStart } from '../../../../src/plugins/dashboard/public';
|
||||
import { IndexPatternFieldEditorStart } from '../../../../src/plugins/index_pattern_field_editor/public';
|
||||
|
||||
export interface SetupPlugins {
|
||||
home?: HomePublicPluginSetup;
|
||||
|
@ -67,6 +68,7 @@ export interface StartPlugins {
|
|||
uiActions: UiActionsStart;
|
||||
ml?: MlPluginStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
indexPatternFieldEditor: IndexPatternFieldEditorStart;
|
||||
}
|
||||
|
||||
export type StartServices = CoreStart &
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { ComponentType, JSXElementConstructor } from 'react';
|
||||
import { EuiDataGridControlColumn, EuiDataGridCellValueElementProps } from '@elastic/eui';
|
||||
|
||||
import { OnRowSelected, SortColumnTimeline, TimelineTabs } from '..';
|
||||
import { CreateFieldComponentType, OnRowSelected, SortColumnTimeline, TimelineTabs } from '..';
|
||||
import { BrowserFields } from '../../../search_strategy/index_fields';
|
||||
import { ColumnHeaderOptions } from '../columns';
|
||||
import { TimelineNonEcsData } from '../../../search_strategy';
|
||||
|
@ -67,6 +67,7 @@ export interface HeaderActionProps {
|
|||
width: number;
|
||||
browserFields: BrowserFields;
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
createFieldComponent?: CreateFieldComponentType;
|
||||
isEventViewer?: boolean;
|
||||
isSelectAllChecked: boolean;
|
||||
onSelectAll: ({ isSelected }: { isSelected: boolean }) => void;
|
||||
|
|
|
@ -467,6 +467,10 @@ export enum TimelineTabs {
|
|||
eql = 'eql',
|
||||
}
|
||||
|
||||
export type CreateFieldComponentType = React.FC<{
|
||||
onClick: () => void;
|
||||
}>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type EmptyObject = Partial<Record<any, never>>;
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ import {
|
|||
TimelineTabs,
|
||||
SetEventsLoading,
|
||||
SetEventsDeleted,
|
||||
CreateFieldComponentType,
|
||||
} from '../../../../common/types/timeline';
|
||||
|
||||
import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
|
||||
|
@ -88,6 +89,7 @@ interface OwnProps {
|
|||
additionalControls?: React.ReactNode;
|
||||
browserFields: BrowserFields;
|
||||
bulkActions?: BulkActionsProp;
|
||||
createFieldComponent?: CreateFieldComponentType;
|
||||
data: TimelineItem[];
|
||||
defaultCellActions?: TGridCellAction[];
|
||||
filters?: Filter[];
|
||||
|
@ -156,6 +158,7 @@ const hasCellActions = (columnId?: string) =>
|
|||
const transformControlColumns = ({
|
||||
columnHeaders,
|
||||
controlColumns,
|
||||
createFieldComponent,
|
||||
data,
|
||||
isEventViewer = false,
|
||||
loadingEventIds,
|
||||
|
@ -177,6 +180,7 @@ const transformControlColumns = ({
|
|||
}: {
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
controlColumns: ControlColumnProps[];
|
||||
createFieldComponent?: CreateFieldComponentType;
|
||||
data: TimelineItem[];
|
||||
isEventViewer?: boolean;
|
||||
loadingEventIds: string[];
|
||||
|
@ -222,6 +226,7 @@ const transformControlColumns = ({
|
|||
sort={sort}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
createFieldComponent={createFieldComponent}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -303,6 +308,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
bulkActions = true,
|
||||
clearSelected,
|
||||
columnHeaders,
|
||||
createFieldComponent,
|
||||
data,
|
||||
defaultCellActions,
|
||||
filterQuery,
|
||||
|
@ -486,6 +492,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
<StatefulFieldsBrowser
|
||||
data-test-subj="field-browser"
|
||||
browserFields={browserFields}
|
||||
createFieldComponent={createFieldComponent}
|
||||
timelineId={id}
|
||||
columnHeaders={columnHeaders}
|
||||
/>
|
||||
|
@ -522,6 +529,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
additionalControls,
|
||||
browserFields,
|
||||
columnHeaders,
|
||||
createFieldComponent,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -611,6 +619,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
transformControlColumns({
|
||||
columnHeaders,
|
||||
controlColumns,
|
||||
createFieldComponent,
|
||||
data,
|
||||
isEventViewer,
|
||||
loadingEventIds,
|
||||
|
@ -636,6 +645,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
columnHeaders,
|
||||
createFieldComponent,
|
||||
data,
|
||||
isEventViewer,
|
||||
id,
|
||||
|
|
|
@ -21,6 +21,7 @@ import type { CoreStart } from '../../../../../../../src/core/public';
|
|||
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
|
||||
import {
|
||||
BulkActionsProp,
|
||||
CreateFieldComponentType,
|
||||
TGridCellAction,
|
||||
TimelineId,
|
||||
TimelineTabs,
|
||||
|
@ -96,6 +97,7 @@ export interface TGridIntegratedProps {
|
|||
browserFields: BrowserFields;
|
||||
bulkActions?: BulkActionsProp;
|
||||
columns: ColumnHeaderOptions[];
|
||||
createFieldComponent?: CreateFieldComponentType;
|
||||
data?: DataPublicPluginStart;
|
||||
dataProviders: DataProvider[];
|
||||
defaultCellActions?: TGridCellAction[];
|
||||
|
@ -150,6 +152,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
globalFullScreen,
|
||||
graphEventId,
|
||||
graphOverlay = null,
|
||||
createFieldComponent,
|
||||
hasAlertsCrud,
|
||||
id,
|
||||
indexNames,
|
||||
|
@ -347,6 +350,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
activePage={pageInfo.activePage}
|
||||
browserFields={browserFields}
|
||||
bulkActions={bulkActions}
|
||||
createFieldComponent={createFieldComponent}
|
||||
data={nonDeletedEvents}
|
||||
defaultCellActions={defaultCellActions}
|
||||
filterQuery={filterQuery}
|
||||
|
|
|
@ -271,4 +271,29 @@ describe('FieldsBrowser', () => {
|
|||
|
||||
expect(onSearchInputChange).toBeCalledWith(inputText);
|
||||
});
|
||||
|
||||
test('it renders the CreateField button when createFieldComponent is provided', () => {
|
||||
const MyTestComponent = () => <div>{'test'}</div>;
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FieldsBrowser
|
||||
columnHeaders={[]}
|
||||
browserFields={mockBrowserFields}
|
||||
filteredBrowserFields={mockBrowserFields}
|
||||
searchInput={''}
|
||||
isSearching={false}
|
||||
onCategorySelected={jest.fn()}
|
||||
onHide={jest.fn()}
|
||||
onSearchInputChange={jest.fn()}
|
||||
restoreFocusTo={React.createRef<HTMLButtonElement>()}
|
||||
selectedCategoryId={''}
|
||||
timelineId={timelineId}
|
||||
createFieldComponent={MyTestComponent}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(MyTestComponent).exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,11 +21,16 @@ import React, { useEffect, useCallback, useRef, useMemo } from 'react';
|
|||
import styled from 'styled-components';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import type { BrowserFields, ColumnHeaderOptions } from '../../../../../common';
|
||||
import type {
|
||||
BrowserFields,
|
||||
ColumnHeaderOptions,
|
||||
CreateFieldComponentType,
|
||||
} from '../../../../../common';
|
||||
import { isEscape, isTab, stopPropagationAndPreventDefault } from '../../../../../common';
|
||||
import { CategoriesPane } from './categories_pane';
|
||||
import { FieldsPane } from './fields_pane';
|
||||
import { Search } from './search';
|
||||
|
||||
import {
|
||||
CATEGORY_PANE_WIDTH,
|
||||
CLOSE_BUTTON_CLASS_NAME,
|
||||
|
@ -53,6 +58,9 @@ type Props = Pick<FieldBrowserProps, 'timelineId' | 'browserFields' | 'width'> &
|
|||
* The current timeline column headers
|
||||
*/
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
|
||||
createFieldComponent?: CreateFieldComponentType;
|
||||
|
||||
/**
|
||||
* A map of categoryId -> metadata about the fields in that category,
|
||||
* filtered such that the name of every field in the category includes
|
||||
|
@ -99,6 +107,7 @@ type Props = Pick<FieldBrowserProps, 'timelineId' | 'browserFields' | 'width'> &
|
|||
const FieldsBrowserComponent: React.FC<Props> = ({
|
||||
columnHeaders,
|
||||
filteredBrowserFields,
|
||||
createFieldComponent: CreateField,
|
||||
isSearching,
|
||||
onCategorySelected,
|
||||
onSearchInputChange,
|
||||
|
@ -187,14 +196,22 @@ const FieldsBrowserComponent: React.FC<Props> = ({
|
|||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<Search
|
||||
data-test-subj="header"
|
||||
filteredBrowserFields={filteredBrowserFields}
|
||||
isSearching={isSearching}
|
||||
onSearchInputChange={onInputChange}
|
||||
searchInput={searchInput}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<Search
|
||||
data-test-subj="header"
|
||||
filteredBrowserFields={filteredBrowserFields}
|
||||
isSearching={isSearching}
|
||||
onSearchInputChange={onInputChange}
|
||||
searchInput={searchInput}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{CreateField && <CreateField onClick={onHide} />}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
<PanesFlexGroup alignItems="flexStart" gutterSize="none" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -34,6 +34,7 @@ export const StatefulFieldsBrowserComponent: React.FC<FieldBrowserProps> = ({
|
|||
timelineId,
|
||||
columnHeaders,
|
||||
browserFields,
|
||||
createFieldComponent,
|
||||
width,
|
||||
}) => {
|
||||
const customizeColumnsButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
@ -140,6 +141,7 @@ export const StatefulFieldsBrowserComponent: React.FC<FieldBrowserProps> = ({
|
|||
{show && (
|
||||
<FieldsBrowser
|
||||
browserFields={browserFieldsWithDefaultCategory}
|
||||
createFieldComponent={createFieldComponent}
|
||||
columnHeaders={columnHeaders}
|
||||
filteredBrowserFields={
|
||||
filteredBrowserFields != null ? filteredBrowserFields : browserFieldsWithDefaultCategory
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CreateFieldComponentType } from '../../../../../common';
|
||||
import type { BrowserFields } from '../../../../../common/search_strategy/index_fields';
|
||||
import type { ColumnHeaderOptions } from '../../../../../common/types/timeline/columns';
|
||||
|
||||
|
@ -17,6 +18,8 @@ export interface FieldBrowserProps {
|
|||
columnHeaders: ColumnHeaderOptions[];
|
||||
/** A map of categoryId -> metadata about the fields in that category */
|
||||
browserFields: BrowserFields;
|
||||
|
||||
createFieldComponent?: CreateFieldComponentType;
|
||||
/** When true, this Fields Browser is being used as an "events viewer" */
|
||||
isEventViewer?: boolean;
|
||||
/** The width of the field browser */
|
||||
|
|
|
@ -69,3 +69,5 @@ export function plugin() {
|
|||
|
||||
export const StatefulEventContext = createContext<StatefulEventContextType | null>(null);
|
||||
export { TimelineContext } from './components/t_grid/shared';
|
||||
|
||||
export type { CreateFieldComponentType } from '../common';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue