[Security Solution] Accessibility (a11y) fixes (#87783)
## [Security Solution] Accessibility (a11y) fixes This PR fixes the following accessibility (a11y) issues: - Fixes an issue that prevented tabbing through all elements on pages with embedded Timelines - Fixes an issue where the Timeline data providers popover menu was not displayed when Enter is pressed - Fixes an issue where duplicate draggable IDs caused errors when re-arranging Timeline columns - Fixes an issue where Timeline columns could not be removed or sorted via keyboard - Fixes an issue where focus is not restored to the `Customize Columns` button when the `Reset` button is pressed - Fixes an issue where filtering the `Customize Event Renderers` view via the input cleared selected entries - Fixes an issue where the active timeline button wasn't focused when Timeline is closed - Fixes an issue where the `(+)` Create / Open Timeline button's hover panel didn't own focus
This commit is contained in:
parent
5dca937c01
commit
46083c0973
|
@ -16,6 +16,7 @@ import {
|
||||||
FIELDS_BROWSER_SELECTED_CATEGORY_COUNT,
|
FIELDS_BROWSER_SELECTED_CATEGORY_COUNT,
|
||||||
FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT,
|
FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT,
|
||||||
} from '../screens/fields_browser';
|
} from '../screens/fields_browser';
|
||||||
|
import { TIMELINE_FIELDS_BUTTON } from '../screens/timeline';
|
||||||
import { cleanKibana } from '../tasks/common';
|
import { cleanKibana } from '../tasks/common';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -182,5 +183,19 @@ describe('Fields Browser', () => {
|
||||||
|
|
||||||
cy.get(FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER).should('not.exist');
|
cy.get(FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER).should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('restores focus to the Customize Columns button when `Reset Fields` is clicked', () => {
|
||||||
|
openTimelineFieldsBrowser();
|
||||||
|
resetFields();
|
||||||
|
|
||||||
|
cy.get(TIMELINE_FIELDS_BUTTON).should('have.focus');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores focus to the Customize Columns button when Esc is pressed', () => {
|
||||||
|
openTimelineFieldsBrowser();
|
||||||
|
cy.get('body').type('{esc}');
|
||||||
|
|
||||||
|
cy.get(TIMELINE_FIELDS_BUTTON).should('have.focus');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
TIMELINE_DATA_PROVIDERS,
|
TIMELINE_DATA_PROVIDERS,
|
||||||
TIMELINE_DATA_PROVIDERS_EMPTY,
|
TIMELINE_DATA_PROVIDERS_EMPTY,
|
||||||
TIMELINE_DROPPED_DATA_PROVIDERS,
|
TIMELINE_DROPPED_DATA_PROVIDERS,
|
||||||
|
TIMELINE_DATA_PROVIDERS_ACTION_MENU,
|
||||||
} from '../screens/timeline';
|
} from '../screens/timeline';
|
||||||
import { HOSTS_NAMES_DRAGGABLE } from '../screens/hosts/all_hosts';
|
import { HOSTS_NAMES_DRAGGABLE } from '../screens/hosts/all_hosts';
|
||||||
|
|
||||||
|
@ -53,6 +54,17 @@ describe('timeline data providers', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('displays the data provider action menu when Enter is pressed', () => {
|
||||||
|
dragAndDropFirstHostToTimeline();
|
||||||
|
openTimelineUsingToggle();
|
||||||
|
cy.get(TIMELINE_DATA_PROVIDERS_ACTION_MENU).should('not.exist');
|
||||||
|
|
||||||
|
cy.get(TIMELINE_DROPPED_DATA_PROVIDERS).first().focus();
|
||||||
|
cy.get(TIMELINE_DROPPED_DATA_PROVIDERS).first().parent().type('{enter}');
|
||||||
|
|
||||||
|
cy.get(TIMELINE_DATA_PROVIDERS_ACTION_MENU).should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
it('sets the background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the data providers', () => {
|
it('sets the background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the data providers', () => {
|
||||||
dragFirstHostToTimeline();
|
dragFirstHostToTimeline();
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,22 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TIMELINE_FLYOUT_HEADER, TIMELINE_DATA_PROVIDERS } from '../screens/timeline';
|
import { TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON } from '../screens/security_main';
|
||||||
|
import {
|
||||||
|
CREATE_NEW_TIMELINE,
|
||||||
|
TIMELINE_DATA_PROVIDERS,
|
||||||
|
TIMELINE_FLYOUT_HEADER,
|
||||||
|
TIMELINE_SETTINGS_ICON,
|
||||||
|
} from '../screens/timeline';
|
||||||
import { cleanKibana } from '../tasks/common';
|
import { cleanKibana } from '../tasks/common';
|
||||||
|
|
||||||
import { dragFirstHostToTimeline, waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts';
|
import { dragFirstHostToTimeline, waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts';
|
||||||
import { loginAndWaitForPage } from '../tasks/login';
|
import { loginAndWaitForPage } from '../tasks/login';
|
||||||
import { openTimelineUsingToggle, closeTimelineUsingToggle } from '../tasks/security_main';
|
import {
|
||||||
|
closeTimelineUsingCloseButton,
|
||||||
|
closeTimelineUsingToggle,
|
||||||
|
openTimelineUsingToggle,
|
||||||
|
} from '../tasks/security_main';
|
||||||
|
|
||||||
import { HOSTS_URL } from '../urls/navigation';
|
import { HOSTS_URL } from '../urls/navigation';
|
||||||
|
|
||||||
|
@ -26,6 +36,34 @@ describe('timeline flyout button', () => {
|
||||||
closeTimelineUsingToggle();
|
closeTimelineUsingToggle();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('re-focuses the toggle button when timeline is closed by clicking the active timeline toggle button', () => {
|
||||||
|
openTimelineUsingToggle();
|
||||||
|
closeTimelineUsingToggle();
|
||||||
|
|
||||||
|
cy.get(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).should('have.focus');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('re-focuses the toggle button when timeline is closed by clicking the [X] close button', () => {
|
||||||
|
openTimelineUsingToggle();
|
||||||
|
closeTimelineUsingCloseButton();
|
||||||
|
|
||||||
|
cy.get(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).should('have.focus');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('re-focuses the toggle button when timeline is closed by pressing the Esc key', () => {
|
||||||
|
openTimelineUsingToggle();
|
||||||
|
cy.get('body').type('{esc}');
|
||||||
|
|
||||||
|
cy.get(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).should('have.focus');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('the `(+)` button popover menu owns focus', () => {
|
||||||
|
cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').click({ force: true });
|
||||||
|
cy.get(CREATE_NEW_TIMELINE).should('have.focus');
|
||||||
|
cy.get('body').type('{esc}');
|
||||||
|
cy.get(CREATE_NEW_TIMELINE).should('not.be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
it('sets the data providers background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the data providers area', () => {
|
it('sets the data providers background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the data providers area', () => {
|
||||||
dragFirstHostToTimeline();
|
dragFirstHostToTimeline();
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export const CLOSE_TIMELINE_BUTTON = '[data-test-subj="close-timeline"]';
|
||||||
|
|
||||||
export const MAIN_PAGE = '[data-test-subj="kibanaChrome"]';
|
export const MAIN_PAGE = '[data-test-subj="kibanaChrome"]';
|
||||||
|
|
||||||
export const TIMELINE_TOGGLE_BUTTON = '[data-test-subj="flyoutOverlay"]';
|
export const TIMELINE_TOGGLE_BUTTON = '[data-test-subj="flyoutOverlay"]';
|
||||||
|
|
|
@ -111,6 +111,8 @@ export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinne
|
||||||
|
|
||||||
export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]';
|
export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]';
|
||||||
|
|
||||||
|
export const TIMELINE_DATA_PROVIDERS_ACTION_MENU = '[data-test-subj="providerActions"]';
|
||||||
|
|
||||||
export const TIMELINE_DATA_PROVIDERS_EMPTY =
|
export const TIMELINE_DATA_PROVIDERS_EMPTY =
|
||||||
'[data-test-subj="dataProviders"] [data-test-subj="empty"]';
|
'[data-test-subj="dataProviders"] [data-test-subj="empty"]';
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CLOSE_TIMELINE_BUTTON,
|
||||||
MAIN_PAGE,
|
MAIN_PAGE,
|
||||||
TIMELINE_TOGGLE_BUTTON,
|
TIMELINE_TOGGLE_BUTTON,
|
||||||
TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON,
|
TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON,
|
||||||
|
@ -18,6 +19,10 @@ export const closeTimelineUsingToggle = () => {
|
||||||
cy.get(TIMELINE_TOGGLE_BUTTON).filter(':visible').click();
|
cy.get(TIMELINE_TOGGLE_BUTTON).filter(':visible').click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const closeTimelineUsingCloseButton = () => {
|
||||||
|
cy.get(CLOSE_TIMELINE_BUTTON).filter(':visible').click();
|
||||||
|
};
|
||||||
|
|
||||||
export const openTimelineIfClosed = () =>
|
export const openTimelineIfClosed = () =>
|
||||||
cy.get(MAIN_PAGE).then(($page) => {
|
cy.get(MAIN_PAGE).then(($page) => {
|
||||||
if ($page.find(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).length === 1) {
|
if ($page.find(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).length === 1) {
|
||||||
|
|
|
@ -220,7 +220,7 @@ const AlertsUtilityBarComponent: React.FC<AlertsUtilityBarProps> = ({
|
||||||
disabled={areEventsLoading}
|
disabled={areEventsLoading}
|
||||||
iconType="arrowDown"
|
iconType="arrowDown"
|
||||||
iconSide="right"
|
iconSide="right"
|
||||||
ownFocus={false}
|
ownFocus={true}
|
||||||
popoverContent={UtilityBarAdditionalFiltersContent}
|
popoverContent={UtilityBarAdditionalFiltersContent}
|
||||||
>
|
>
|
||||||
{i18n.ADDITIONAL_FILTERS_ACTIONS}
|
{i18n.ADDITIONAL_FILTERS_ACTIONS}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import { EuiSpacer, EuiWindowEvent } from '@elastic/eui';
|
import { EuiSpacer, EuiWindowEvent } from '@elastic/eui';
|
||||||
import { noop } from 'lodash/fp';
|
import { noop } from 'lodash/fp';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/h
|
||||||
import { SecurityPageName } from '../../../app/types';
|
import { SecurityPageName } from '../../../app/types';
|
||||||
import { TimelineId } from '../../../../common/types/timeline';
|
import { TimelineId } from '../../../../common/types/timeline';
|
||||||
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
||||||
|
import { isTab } from '../../../common/components/accessibility/helpers';
|
||||||
import { UpdateDateRange } from '../../../common/components/charts/common';
|
import { UpdateDateRange } from '../../../common/components/charts/common';
|
||||||
import { FiltersGlobal } from '../../../common/components/filters_global';
|
import { FiltersGlobal } from '../../../common/components/filters_global';
|
||||||
import { getRulesUrl } from '../../../common/components/link_to/redirect_to_detection_engine';
|
import { getRulesUrl } from '../../../common/components/link_to/redirect_to_detection_engine';
|
||||||
|
@ -39,7 +40,12 @@ import { LinkButton } from '../../../common/components/links';
|
||||||
import { useFormatUrl } from '../../../common/components/link_to';
|
import { useFormatUrl } from '../../../common/components/link_to';
|
||||||
import { useGlobalFullScreen } from '../../../common/containers/use_full_screen';
|
import { useGlobalFullScreen } from '../../../common/containers/use_full_screen';
|
||||||
import { Display } from '../../../hosts/pages/display';
|
import { Display } from '../../../hosts/pages/display';
|
||||||
import { showGlobalFilters } from '../../../timelines/components/timeline/helpers';
|
import {
|
||||||
|
focusUtilityBarAction,
|
||||||
|
onTimelineTabKeyPressed,
|
||||||
|
resetKeyboardFocus,
|
||||||
|
showGlobalFilters,
|
||||||
|
} from '../../../timelines/components/timeline/helpers';
|
||||||
import { timelineSelectors } from '../../../timelines/store/timeline';
|
import { timelineSelectors } from '../../../timelines/store/timeline';
|
||||||
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
|
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
|
||||||
import { buildShowBuildingBlockFilter } from '../../components/alerts_table/default_config';
|
import { buildShowBuildingBlockFilter } from '../../components/alerts_table/default_config';
|
||||||
|
@ -48,6 +54,7 @@ import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||||
|
|
||||||
const DetectionEnginePageComponent = () => {
|
const DetectionEnginePageComponent = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const containerElement = useRef<HTMLDivElement | null>(null);
|
||||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||||
const graphEventId = useShallowEqualSelector(
|
const graphEventId = useShallowEqualSelector(
|
||||||
(state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).graphEventId
|
(state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).graphEventId
|
||||||
|
@ -128,6 +135,28 @@ const DetectionEnginePageComponent = () => {
|
||||||
|
|
||||||
const { indicesExist, indexPattern } = useSourcererScope(SourcererScopeName.detections);
|
const { indicesExist, indexPattern } = useSourcererScope(SourcererScopeName.detections);
|
||||||
|
|
||||||
|
const onSkipFocusBeforeEventsTable = useCallback(() => {
|
||||||
|
focusUtilityBarAction(containerElement.current);
|
||||||
|
}, [containerElement]);
|
||||||
|
|
||||||
|
const onSkipFocusAfterEventsTable = useCallback(() => {
|
||||||
|
resetKeyboardFocus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback(
|
||||||
|
(keyboardEvent: React.KeyboardEvent) => {
|
||||||
|
if (isTab(keyboardEvent)) {
|
||||||
|
onTimelineTabKeyPressed({
|
||||||
|
containerElement: containerElement.current,
|
||||||
|
keyboardEvent,
|
||||||
|
onSkipFocusBeforeEventsTable,
|
||||||
|
onSkipFocusAfterEventsTable,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable]
|
||||||
|
);
|
||||||
|
|
||||||
if (isUserAuthenticated != null && !isUserAuthenticated && !loading) {
|
if (isUserAuthenticated != null && !isUserAuthenticated && !loading) {
|
||||||
return (
|
return (
|
||||||
<WrapperPage>
|
<WrapperPage>
|
||||||
|
@ -154,7 +183,7 @@ const DetectionEnginePageComponent = () => {
|
||||||
{hasEncryptionKey != null && !hasEncryptionKey && <NoApiIntegrationKeyCallOut />}
|
{hasEncryptionKey != null && !hasEncryptionKey && <NoApiIntegrationKeyCallOut />}
|
||||||
<ReadOnlyAlertsCallOut />
|
<ReadOnlyAlertsCallOut />
|
||||||
{indicesExist ? (
|
{indicesExist ? (
|
||||||
<>
|
<div onKeyDown={onKeyDown} ref={containerElement}>
|
||||||
<EuiWindowEvent event="resize" handler={noop} />
|
<EuiWindowEvent event="resize" handler={noop} />
|
||||||
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
|
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
|
||||||
<SiemSearchBar id="global" indexPattern={indexPattern} />
|
<SiemSearchBar id="global" indexPattern={indexPattern} />
|
||||||
|
@ -211,7 +240,7 @@ const DetectionEnginePageComponent = () => {
|
||||||
to={to}
|
to={to}
|
||||||
/>
|
/>
|
||||||
</WrapperPage>
|
</WrapperPage>
|
||||||
</>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<WrapperPage>
|
<WrapperPage>
|
||||||
<DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} />
|
<DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} />
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { noop } from 'lodash/fp';
|
import { noop } from 'lodash/fp';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useParams, useHistory } from 'react-router-dom';
|
import { useParams, useHistory } from 'react-router-dom';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
@ -81,7 +81,12 @@ import { useGlobalFullScreen } from '../../../../../common/containers/use_full_s
|
||||||
import { Display } from '../../../../../hosts/pages/display';
|
import { Display } from '../../../../../hosts/pages/display';
|
||||||
import { ExceptionListTypeEnum, ExceptionListIdentifiers } from '../../../../../shared_imports';
|
import { ExceptionListTypeEnum, ExceptionListIdentifiers } from '../../../../../shared_imports';
|
||||||
import { useRuleAsync } from '../../../../containers/detection_engine/rules/use_rule_async';
|
import { useRuleAsync } from '../../../../containers/detection_engine/rules/use_rule_async';
|
||||||
import { showGlobalFilters } from '../../../../../timelines/components/timeline/helpers';
|
import {
|
||||||
|
focusUtilityBarAction,
|
||||||
|
onTimelineTabKeyPressed,
|
||||||
|
resetKeyboardFocus,
|
||||||
|
showGlobalFilters,
|
||||||
|
} from '../../../../../timelines/components/timeline/helpers';
|
||||||
import { timelineSelectors } from '../../../../../timelines/store/timeline';
|
import { timelineSelectors } from '../../../../../timelines/store/timeline';
|
||||||
import { timelineDefaults } from '../../../../../timelines/store/timeline/defaults';
|
import { timelineDefaults } from '../../../../../timelines/store/timeline/defaults';
|
||||||
import { useSourcererScope } from '../../../../../common/containers/sourcerer';
|
import { useSourcererScope } from '../../../../../common/containers/sourcerer';
|
||||||
|
@ -95,6 +100,7 @@ import {
|
||||||
import * as detectionI18n from '../../translations';
|
import * as detectionI18n from '../../translations';
|
||||||
import * as ruleI18n from '../translations';
|
import * as ruleI18n from '../translations';
|
||||||
import * as i18n from './translations';
|
import * as i18n from './translations';
|
||||||
|
import { isTab } from '../../../../../common/components/accessibility/helpers';
|
||||||
|
|
||||||
enum RuleDetailTabs {
|
enum RuleDetailTabs {
|
||||||
alerts = 'alerts',
|
alerts = 'alerts',
|
||||||
|
@ -127,6 +133,7 @@ const getRuleDetailsTabs = (rule: Rule | null) => {
|
||||||
|
|
||||||
const RuleDetailsPageComponent = () => {
|
const RuleDetailsPageComponent = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const containerElement = useRef<HTMLDivElement | null>(null);
|
||||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||||
const graphEventId = useShallowEqualSelector(
|
const graphEventId = useShallowEqualSelector(
|
||||||
(state) =>
|
(state) =>
|
||||||
|
@ -408,6 +415,28 @@ const RuleDetailsPageComponent = () => {
|
||||||
}
|
}
|
||||||
}, [rule]);
|
}, [rule]);
|
||||||
|
|
||||||
|
const onSkipFocusBeforeEventsTable = useCallback(() => {
|
||||||
|
focusUtilityBarAction(containerElement.current);
|
||||||
|
}, [containerElement]);
|
||||||
|
|
||||||
|
const onSkipFocusAfterEventsTable = useCallback(() => {
|
||||||
|
resetKeyboardFocus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback(
|
||||||
|
(keyboardEvent: React.KeyboardEvent) => {
|
||||||
|
if (isTab(keyboardEvent)) {
|
||||||
|
onTimelineTabKeyPressed({
|
||||||
|
containerElement: containerElement.current,
|
||||||
|
keyboardEvent,
|
||||||
|
onSkipFocusBeforeEventsTable,
|
||||||
|
onSkipFocusAfterEventsTable,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable]
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
redirectToDetections(
|
redirectToDetections(
|
||||||
isSignalIndexExists,
|
isSignalIndexExists,
|
||||||
|
@ -430,7 +459,7 @@ const RuleDetailsPageComponent = () => {
|
||||||
<ReadOnlyAlertsCallOut />
|
<ReadOnlyAlertsCallOut />
|
||||||
<ReadOnlyRulesCallOut />
|
<ReadOnlyRulesCallOut />
|
||||||
{indicesExist ? (
|
{indicesExist ? (
|
||||||
<>
|
<div onKeyDown={onKeyDown} ref={containerElement}>
|
||||||
<EuiWindowEvent event="resize" handler={noop} />
|
<EuiWindowEvent event="resize" handler={noop} />
|
||||||
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
|
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
|
||||||
<SiemSearchBar id="global" indexPattern={indexPattern} />
|
<SiemSearchBar id="global" indexPattern={indexPattern} />
|
||||||
|
@ -588,7 +617,7 @@ const RuleDetailsPageComponent = () => {
|
||||||
)}
|
)}
|
||||||
{ruleDetailTab === RuleDetailTabs.failures && <FailureHistory id={rule?.id} />}
|
{ruleDetailTab === RuleDetailTabs.failures && <FailureHistory id={rule?.id} />}
|
||||||
</WrapperPage>
|
</WrapperPage>
|
||||||
</>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<WrapperPage>
|
<WrapperPage>
|
||||||
<DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} />
|
<DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} />
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import { EuiSpacer, EuiWindowEvent } from '@elastic/eui';
|
import { EuiSpacer, EuiWindowEvent } from '@elastic/eui';
|
||||||
import { noop } from 'lodash/fp';
|
import { noop } from 'lodash/fp';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo, useRef } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -40,7 +40,12 @@ import * as i18n from './translations';
|
||||||
import { filterHostData } from './navigation';
|
import { filterHostData } from './navigation';
|
||||||
import { hostsModel } from '../store';
|
import { hostsModel } from '../store';
|
||||||
import { HostsTableType } from '../store/model';
|
import { HostsTableType } from '../store/model';
|
||||||
import { showGlobalFilters } from '../../timelines/components/timeline/helpers';
|
import { isTab } from '../../common/components/accessibility/helpers';
|
||||||
|
import {
|
||||||
|
onTimelineTabKeyPressed,
|
||||||
|
resetKeyboardFocus,
|
||||||
|
showGlobalFilters,
|
||||||
|
} from '../../timelines/components/timeline/helpers';
|
||||||
import { timelineSelectors } from '../../timelines/store/timeline';
|
import { timelineSelectors } from '../../timelines/store/timeline';
|
||||||
import { timelineDefaults } from '../../timelines/store/timeline/defaults';
|
import { timelineDefaults } from '../../timelines/store/timeline/defaults';
|
||||||
import { useSourcererScope } from '../../common/containers/sourcerer';
|
import { useSourcererScope } from '../../common/containers/sourcerer';
|
||||||
|
@ -48,6 +53,7 @@ import { useDeepEqualSelector, useShallowEqualSelector } from '../../common/hook
|
||||||
|
|
||||||
const HostsComponent = () => {
|
const HostsComponent = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const containerElement = useRef<HTMLDivElement | null>(null);
|
||||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||||
const graphEventId = useShallowEqualSelector(
|
const graphEventId = useShallowEqualSelector(
|
||||||
(state) =>
|
(state) =>
|
||||||
|
@ -114,10 +120,34 @@ const HostsComponent = () => {
|
||||||
[indexPattern, query, tabsFilters, uiSettings]
|
[indexPattern, query, tabsFilters, uiSettings]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onSkipFocusBeforeEventsTable = useCallback(() => {
|
||||||
|
containerElement.current
|
||||||
|
?.querySelector<HTMLButtonElement>('.inspectButtonComponent:last-of-type')
|
||||||
|
?.focus();
|
||||||
|
}, [containerElement]);
|
||||||
|
|
||||||
|
const onSkipFocusAfterEventsTable = useCallback(() => {
|
||||||
|
resetKeyboardFocus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback(
|
||||||
|
(keyboardEvent: React.KeyboardEvent) => {
|
||||||
|
if (isTab(keyboardEvent)) {
|
||||||
|
onTimelineTabKeyPressed({
|
||||||
|
containerElement: containerElement.current,
|
||||||
|
keyboardEvent,
|
||||||
|
onSkipFocusBeforeEventsTable,
|
||||||
|
onSkipFocusAfterEventsTable,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{indicesExist ? (
|
{indicesExist ? (
|
||||||
<>
|
<div onKeyDown={onKeyDown} ref={containerElement}>
|
||||||
<EuiWindowEvent event="resize" handler={noop} />
|
<EuiWindowEvent event="resize" handler={noop} />
|
||||||
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
|
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
|
||||||
<SiemSearchBar indexPattern={indexPattern} id="global" />
|
<SiemSearchBar indexPattern={indexPattern} id="global" />
|
||||||
|
@ -167,7 +197,7 @@ const HostsComponent = () => {
|
||||||
type={hostsModel.HostsType.page}
|
type={hostsModel.HostsType.page}
|
||||||
/>
|
/>
|
||||||
</WrapperPage>
|
</WrapperPage>
|
||||||
</>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<WrapperPage>
|
<WrapperPage>
|
||||||
<HeaderPage border title={i18n.PAGE_TITLE} />
|
<HeaderPage border title={i18n.PAGE_TITLE} />
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import { EuiSpacer, EuiWindowEvent } from '@elastic/eui';
|
import { EuiSpacer, EuiWindowEvent } from '@elastic/eui';
|
||||||
import { noop } from 'lodash/fp';
|
import { noop } from 'lodash/fp';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo, useRef } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -38,8 +38,13 @@ import { OverviewEmpty } from '../../overview/components/overview_empty';
|
||||||
import * as i18n from './translations';
|
import * as i18n from './translations';
|
||||||
import { NetworkComponentProps } from './types';
|
import { NetworkComponentProps } from './types';
|
||||||
import { NetworkRouteType } from './navigation/types';
|
import { NetworkRouteType } from './navigation/types';
|
||||||
import { showGlobalFilters } from '../../timelines/components/timeline/helpers';
|
import {
|
||||||
|
onTimelineTabKeyPressed,
|
||||||
|
resetKeyboardFocus,
|
||||||
|
showGlobalFilters,
|
||||||
|
} from '../../timelines/components/timeline/helpers';
|
||||||
import { timelineSelectors } from '../../timelines/store/timeline';
|
import { timelineSelectors } from '../../timelines/store/timeline';
|
||||||
|
import { isTab } from '../../common/components/accessibility/helpers';
|
||||||
import { TimelineId } from '../../../common/types/timeline';
|
import { TimelineId } from '../../../common/types/timeline';
|
||||||
import { timelineDefaults } from '../../timelines/store/timeline/defaults';
|
import { timelineDefaults } from '../../timelines/store/timeline/defaults';
|
||||||
import { useSourcererScope } from '../../common/containers/sourcerer';
|
import { useSourcererScope } from '../../common/containers/sourcerer';
|
||||||
|
@ -48,6 +53,7 @@ import { useDeepEqualSelector, useShallowEqualSelector } from '../../common/hook
|
||||||
const NetworkComponent = React.memo<NetworkComponentProps>(
|
const NetworkComponent = React.memo<NetworkComponentProps>(
|
||||||
({ networkPagePath, hasMlUserPermissions, capabilitiesFetched }) => {
|
({ networkPagePath, hasMlUserPermissions, capabilitiesFetched }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const containerElement = useRef<HTMLDivElement | null>(null);
|
||||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||||
const graphEventId = useShallowEqualSelector(
|
const graphEventId = useShallowEqualSelector(
|
||||||
(state) =>
|
(state) =>
|
||||||
|
@ -91,6 +97,31 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
|
||||||
);
|
);
|
||||||
|
|
||||||
const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope();
|
const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope();
|
||||||
|
|
||||||
|
const onSkipFocusBeforeEventsTable = useCallback(() => {
|
||||||
|
containerElement.current
|
||||||
|
?.querySelector<HTMLButtonElement>('.inspectButtonComponent:last-of-type')
|
||||||
|
?.focus();
|
||||||
|
}, [containerElement]);
|
||||||
|
|
||||||
|
const onSkipFocusAfterEventsTable = useCallback(() => {
|
||||||
|
resetKeyboardFocus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback(
|
||||||
|
(keyboardEvent: React.KeyboardEvent) => {
|
||||||
|
if (isTab(keyboardEvent)) {
|
||||||
|
onTimelineTabKeyPressed({
|
||||||
|
containerElement: containerElement.current,
|
||||||
|
keyboardEvent,
|
||||||
|
onSkipFocusBeforeEventsTable,
|
||||||
|
onSkipFocusAfterEventsTable,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable]
|
||||||
|
);
|
||||||
|
|
||||||
const filterQuery = convertToBuildEsQuery({
|
const filterQuery = convertToBuildEsQuery({
|
||||||
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
|
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
|
||||||
indexPattern,
|
indexPattern,
|
||||||
|
@ -107,7 +138,7 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{indicesExist ? (
|
{indicesExist ? (
|
||||||
<>
|
<div onKeyDown={onKeyDown} ref={containerElement}>
|
||||||
<EuiWindowEvent event="resize" handler={noop} />
|
<EuiWindowEvent event="resize" handler={noop} />
|
||||||
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
|
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
|
||||||
<SiemSearchBar indexPattern={indexPattern} id="global" />
|
<SiemSearchBar indexPattern={indexPattern} id="global" />
|
||||||
|
@ -176,7 +207,7 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
|
||||||
<NetworkRoutesLoading />
|
<NetworkRoutesLoading />
|
||||||
)}
|
)}
|
||||||
</WrapperPage>
|
</WrapperPage>
|
||||||
</>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<WrapperPage>
|
<WrapperPage>
|
||||||
<HeaderPage border title={i18n.PAGE_TITLE} />
|
<HeaderPage border title={i18n.PAGE_TITLE} />
|
||||||
|
|
|
@ -239,7 +239,7 @@ const FieldsBrowserComponent: React.FC<Props> = ({
|
||||||
data-test-subj="header"
|
data-test-subj="header"
|
||||||
filteredBrowserFields={filteredBrowserFields}
|
filteredBrowserFields={filteredBrowserFields}
|
||||||
isSearching={isSearching}
|
isSearching={isSearching}
|
||||||
onOutsideClick={onOutsideClick}
|
onOutsideClick={closeAndRestoreFocus}
|
||||||
onSearchInputChange={onInputChange}
|
onSearchInputChange={onInputChange}
|
||||||
onUpdateColumns={onUpdateColumns}
|
onUpdateColumns={onUpdateColumns}
|
||||||
searchInput={searchInput}
|
searchInput={searchInput}
|
||||||
|
|
|
@ -55,6 +55,7 @@ const AddTimelineButtonComponent: React.FC<AddTimelineButtonComponentProps> = ({
|
||||||
id="timelineSettingsPopover"
|
id="timelineSettingsPopover"
|
||||||
isOpen={showActions}
|
isOpen={showActions}
|
||||||
closePopover={onClosePopover}
|
closePopover={onClosePopover}
|
||||||
|
ownFocus
|
||||||
repositionOnScroll
|
repositionOnScroll
|
||||||
>
|
>
|
||||||
<EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="none">
|
<EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="none">
|
||||||
|
|
|
@ -13,11 +13,10 @@ import { IS_DRAGGING_CLASS_NAME } from '../../../../common/components/drag_and_d
|
||||||
import { DataProvider } from '../../timeline/data_providers/data_provider';
|
import { DataProvider } from '../../timeline/data_providers/data_provider';
|
||||||
import { flattenIntoAndGroups } from '../../timeline/data_providers/helpers';
|
import { flattenIntoAndGroups } from '../../timeline/data_providers/helpers';
|
||||||
import { DataProviders } from '../../timeline/data_providers';
|
import { DataProviders } from '../../timeline/data_providers';
|
||||||
|
import { FLYOUT_BUTTON_BAR_CLASS_NAME, FLYOUT_BUTTON_CLASS_NAME } from '../../timeline/helpers';
|
||||||
import { FlyoutHeaderPanel } from '../header';
|
import { FlyoutHeaderPanel } from '../header';
|
||||||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||||
|
|
||||||
export const FLYOUT_BUTTON_CLASS_NAME = 'timeline-flyout-button';
|
|
||||||
|
|
||||||
export const getBadgeCount = (dataProviders: DataProvider[]): number =>
|
export const getBadgeCount = (dataProviders: DataProvider[]): number =>
|
||||||
flattenIntoAndGroups(dataProviders).reduce((total, group) => total + group.length, 0);
|
flattenIntoAndGroups(dataProviders).reduce((total, group) => total + group.length, 0);
|
||||||
|
|
||||||
|
@ -76,7 +75,11 @@ interface FlyoutBottomBarProps {
|
||||||
export const FlyoutBottomBar = React.memo<FlyoutBottomBarProps>(
|
export const FlyoutBottomBar = React.memo<FlyoutBottomBarProps>(
|
||||||
({ activeTab, showDataproviders, timelineId }) => {
|
({ activeTab, showDataproviders, timelineId }) => {
|
||||||
return (
|
return (
|
||||||
<Container $isGlobal={showDataproviders} data-test-subj="flyoutBottomBar">
|
<Container
|
||||||
|
className={FLYOUT_BUTTON_BAR_CLASS_NAME}
|
||||||
|
$isGlobal={showDataproviders}
|
||||||
|
data-test-subj="flyoutBottomBar"
|
||||||
|
>
|
||||||
{showDataproviders && <FlyoutHeaderPanel timelineId={timelineId} />}
|
{showDataproviders && <FlyoutHeaderPanel timelineId={timelineId} />}
|
||||||
{(showDataproviders || (!showDataproviders && activeTab !== TimelineTabs.query)) && (
|
{(showDataproviders || (!showDataproviders && activeTab !== TimelineTabs.query)) && (
|
||||||
<DataProvidersPanel paddingSize="none">
|
<DataProvidersPanel paddingSize="none">
|
||||||
|
|
|
@ -13,6 +13,10 @@ import { FormattedRelative } from '@kbn/i18n/react';
|
||||||
|
|
||||||
import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline';
|
import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline';
|
||||||
import { TimelineEventsCountBadge } from '../../../../common/hooks/use_timeline_events_count';
|
import { TimelineEventsCountBadge } from '../../../../common/hooks/use_timeline_events_count';
|
||||||
|
import {
|
||||||
|
ACTIVE_TIMELINE_BUTTON_CLASS_NAME,
|
||||||
|
focusActiveTimelineButton,
|
||||||
|
} from '../../timeline/helpers';
|
||||||
import { UNTITLED_TIMELINE, UNTITLED_TEMPLATE } from '../../timeline/properties/translations';
|
import { UNTITLED_TIMELINE, UNTITLED_TEMPLATE } from '../../timeline/properties/translations';
|
||||||
import { timelineActions } from '../../../store/timeline';
|
import { timelineActions } from '../../../store/timeline';
|
||||||
import * as i18n from './translations';
|
import * as i18n from './translations';
|
||||||
|
@ -50,11 +54,10 @@ const ActiveTimelinesComponent: React.FC<ActiveTimelinesProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const handleToggleOpen = useCallback(() => {
|
||||||
const handleToggleOpen = useCallback(
|
dispatch(timelineActions.showTimeline({ id: timelineId, show: !isOpen }));
|
||||||
() => dispatch(timelineActions.showTimeline({ id: timelineId, show: !isOpen })),
|
focusActiveTimelineButton();
|
||||||
[dispatch, isOpen, timelineId]
|
}, [dispatch, isOpen, timelineId]);
|
||||||
);
|
|
||||||
|
|
||||||
const title = !isEmpty(timelineTitle)
|
const title = !isEmpty(timelineTitle)
|
||||||
? timelineTitle
|
? timelineTitle
|
||||||
|
@ -83,6 +86,7 @@ const ActiveTimelinesComponent: React.FC<ActiveTimelinesProps> = ({
|
||||||
<ButtonWrapper grow={false}>
|
<ButtonWrapper grow={false}>
|
||||||
<StyledEuiButtonEmpty
|
<StyledEuiButtonEmpty
|
||||||
aria-label={i18n.TIMELINE_TOGGLE_BUTTON_ARIA_LABEL({ isOpen, title })}
|
aria-label={i18n.TIMELINE_TOGGLE_BUTTON_ARIA_LABEL({ isOpen, title })}
|
||||||
|
className={ACTIVE_TIMELINE_BUTTON_CLASS_NAME}
|
||||||
flush="both"
|
flush="both"
|
||||||
data-test-subj="flyoutOverlay"
|
data-test-subj="flyoutOverlay"
|
||||||
size="s"
|
size="s"
|
||||||
|
|
|
@ -33,6 +33,7 @@ import { ActiveTimelines } from './active_timelines';
|
||||||
import * as i18n from './translations';
|
import * as i18n from './translations';
|
||||||
import * as commonI18n from '../../timeline/properties/translations';
|
import * as commonI18n from '../../timeline/properties/translations';
|
||||||
import { getTimelineStatusByIdSelector } from './selectors';
|
import { getTimelineStatusByIdSelector } from './selectors';
|
||||||
|
import { focusActiveTimelineButton } from '../../timeline/helpers';
|
||||||
|
|
||||||
// to hide side borders
|
// to hide side borders
|
||||||
const StyledPanel = styled(EuiPanel)`
|
const StyledPanel = styled(EuiPanel)`
|
||||||
|
@ -79,10 +80,10 @@ const FlyoutHeaderPanelComponent: React.FC<FlyoutHeaderPanelProps> = ({ timeline
|
||||||
[dataProviders, kqlQuery]
|
[dataProviders, kqlQuery]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClose = useCallback(
|
const handleClose = useCallback(() => {
|
||||||
() => dispatch(timelineActions.showTimeline({ id: timelineId, show: false })),
|
dispatch(timelineActions.showTimeline({ id: timelineId, show: false }));
|
||||||
[dispatch, timelineId]
|
focusActiveTimelineButton();
|
||||||
);
|
}, [dispatch, timelineId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledPanel
|
<StyledPanel
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { useDispatch } from 'react-redux';
|
||||||
import { StatefulTimeline } from '../../timeline';
|
import { StatefulTimeline } from '../../timeline';
|
||||||
import * as i18n from './translations';
|
import * as i18n from './translations';
|
||||||
import { timelineActions } from '../../../store/timeline';
|
import { timelineActions } from '../../../store/timeline';
|
||||||
|
import { focusActiveTimelineButton } from '../../timeline/helpers';
|
||||||
|
|
||||||
interface FlyoutPaneComponentProps {
|
interface FlyoutPaneComponentProps {
|
||||||
timelineId: string;
|
timelineId: string;
|
||||||
|
@ -28,10 +29,10 @@ const EuiFlyoutContainer = styled.div`
|
||||||
|
|
||||||
const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({ timelineId }) => {
|
const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({ timelineId }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const handleClose = useCallback(
|
const handleClose = useCallback(() => {
|
||||||
() => dispatch(timelineActions.showTimeline({ id: timelineId, show: false })),
|
dispatch(timelineActions.showTimeline({ id: timelineId, show: false }));
|
||||||
[dispatch, timelineId]
|
focusActiveTimelineButton();
|
||||||
);
|
}, [dispatch, timelineId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiFlyoutContainer data-test-subj="flyout-pane">
|
<EuiFlyoutContainer data-test-subj="flyout-pane">
|
||||||
|
|
|
@ -17,18 +17,17 @@ import {
|
||||||
EuiButton,
|
EuiButton,
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
EuiFlexItem,
|
EuiFlexItem,
|
||||||
EuiInMemoryTable,
|
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import React, { useState, useCallback, useMemo, useRef } from 'react';
|
import React, { useState, useCallback, useMemo } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { State } from '../../../common/store';
|
import { State } from '../../../common/store';
|
||||||
|
import { RowRendererId } from '../../../../common/types/timeline';
|
||||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||||
import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../store/timeline/actions';
|
import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../store/timeline/actions';
|
||||||
import { timelineSelectors } from '../../store/timeline';
|
import { timelineSelectors } from '../../store/timeline';
|
||||||
import { timelineDefaults } from '../../store/timeline/defaults';
|
import { timelineDefaults } from '../../store/timeline/defaults';
|
||||||
import { renderers } from './catalog';
|
|
||||||
import { RowRenderersBrowser } from './row_renderers_browser';
|
import { RowRenderersBrowser } from './row_renderers_browser';
|
||||||
import * as i18n from './translations';
|
import * as i18n from './translations';
|
||||||
|
|
||||||
|
@ -81,7 +80,6 @@ interface StatefulRowRenderersBrowserProps {
|
||||||
const StatefulRowRenderersBrowserComponent: React.FC<StatefulRowRenderersBrowserProps> = ({
|
const StatefulRowRenderersBrowserComponent: React.FC<StatefulRowRenderersBrowserProps> = ({
|
||||||
timelineId,
|
timelineId,
|
||||||
}) => {
|
}) => {
|
||||||
const tableRef = useRef<EuiInMemoryTable<{}>>();
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||||
const excludedRowRendererIds = useDeepEqualSelector(
|
const excludedRowRendererIds = useDeepEqualSelector(
|
||||||
|
@ -105,12 +103,12 @@ const StatefulRowRenderersBrowserComponent: React.FC<StatefulRowRenderersBrowser
|
||||||
const hideFieldBrowser = useCallback(() => setShow(false), []);
|
const hideFieldBrowser = useCallback(() => setShow(false), []);
|
||||||
|
|
||||||
const handleDisableAll = useCallback(() => {
|
const handleDisableAll = useCallback(() => {
|
||||||
tableRef?.current?.setSelection([]);
|
setExcludedRowRendererIds(Object.values(RowRendererId));
|
||||||
}, [tableRef]);
|
}, [setExcludedRowRendererIds]);
|
||||||
|
|
||||||
const handleEnableAll = useCallback(() => {
|
const handleEnableAll = useCallback(() => {
|
||||||
tableRef?.current?.setSelection(renderers);
|
setExcludedRowRendererIds([]);
|
||||||
}, [tableRef]);
|
}, [setExcludedRowRendererIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -168,7 +166,6 @@ const StatefulRowRenderersBrowserComponent: React.FC<StatefulRowRenderersBrowser
|
||||||
|
|
||||||
<StyledEuiModalBody>
|
<StyledEuiModalBody>
|
||||||
<RowRenderersBrowser
|
<RowRenderersBrowser
|
||||||
ref={tableRef}
|
|
||||||
excludedRowRendererIds={excludedRowRendererIds}
|
excludedRowRendererIds={excludedRowRendererIds}
|
||||||
setExcludedRowRendererIds={setExcludedRowRendererIds}
|
setExcludedRowRendererIds={setExcludedRowRendererIds}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EuiFlexItem, EuiInMemoryTable } from '@elastic/eui';
|
import { EuiCheckbox, EuiFlexItem, EuiInMemoryTable } from '@elastic/eui';
|
||||||
import React, { useMemo, useCallback } from 'react';
|
import React, { useMemo, useCallback } from 'react';
|
||||||
import { xor, xorBy } from 'lodash/fp';
|
import { xor } from 'lodash/fp';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { RowRendererId } from '../../../../common/types/timeline';
|
import { RowRendererId } from '../../../../common/types/timeline';
|
||||||
|
@ -76,101 +76,89 @@ const StyledNameButton = styled.button`
|
||||||
text-align: left;
|
text-align: left;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const RowRenderersBrowserComponent = React.forwardRef(
|
const RowRenderersBrowserComponent = ({
|
||||||
({ excludedRowRendererIds = [], setExcludedRowRendererIds }: RowRenderersBrowserProps, ref) => {
|
excludedRowRendererIds = [],
|
||||||
const notExcludedRowRenderers = useMemo(() => {
|
setExcludedRowRendererIds,
|
||||||
if (excludedRowRendererIds.length === Object.keys(RowRendererId).length) return [];
|
}: RowRenderersBrowserProps) => {
|
||||||
|
const handleNameClick = useCallback(
|
||||||
|
(item: RowRendererOption) => () => {
|
||||||
|
const newSelection = xor([item.id], excludedRowRendererIds);
|
||||||
|
|
||||||
return renderers.filter((renderer) => !excludedRowRendererIds.includes(renderer.id));
|
setExcludedRowRendererIds(newSelection);
|
||||||
}, [excludedRowRendererIds]);
|
},
|
||||||
|
[excludedRowRendererIds, setExcludedRowRendererIds]
|
||||||
|
);
|
||||||
|
|
||||||
const handleNameClick = useCallback(
|
const nameColumnRenderCallback = useCallback(
|
||||||
(item: RowRendererOption) => () => {
|
(value, item) => (
|
||||||
const newSelection = xor([item], notExcludedRowRenderers);
|
<StyledNameButton className="kbn-resetFocusState" onClick={handleNameClick(item)}>
|
||||||
// @ts-expect-error
|
{value}
|
||||||
ref?.current?.setSelection(newSelection);
|
</StyledNameButton>
|
||||||
},
|
),
|
||||||
[notExcludedRowRenderers, ref]
|
[handleNameClick]
|
||||||
);
|
);
|
||||||
|
|
||||||
const nameColumnRenderCallback = useCallback(
|
const idColumnRenderCallback = useCallback(
|
||||||
(value, item) => (
|
(_, item) => (
|
||||||
<StyledNameButton className="kbn-resetFocusState" onClick={handleNameClick(item)}>
|
<EuiCheckbox
|
||||||
{value}
|
id={item.id}
|
||||||
</StyledNameButton>
|
onChange={handleNameClick(item)}
|
||||||
),
|
checked={!excludedRowRendererIds.includes(item.id)}
|
||||||
[handleNameClick]
|
|
||||||
);
|
|
||||||
|
|
||||||
const columns = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
field: 'name',
|
|
||||||
name: 'Name',
|
|
||||||
sortable: true,
|
|
||||||
width: '10%',
|
|
||||||
render: nameColumnRenderCallback,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'description',
|
|
||||||
name: 'Description',
|
|
||||||
width: '25%',
|
|
||||||
render: (description: React.ReactNode) => description,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'example',
|
|
||||||
name: 'Example',
|
|
||||||
width: '65%',
|
|
||||||
render: ExampleWrapperComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'searchableDescription',
|
|
||||||
name: 'Searchable Description',
|
|
||||||
sortable: false,
|
|
||||||
width: '0px',
|
|
||||||
render: renderSearchableDescriptionNoop,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[nameColumnRenderCallback]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSelectable = useCallback(() => true, []);
|
|
||||||
|
|
||||||
const handleSelectionChange = useCallback(
|
|
||||||
(selection: RowRendererOption[]) => {
|
|
||||||
if (!selection || !selection.length)
|
|
||||||
return setExcludedRowRendererIds(Object.values(RowRendererId));
|
|
||||||
|
|
||||||
const excludedRowRenderers = xorBy('id', renderers, selection);
|
|
||||||
|
|
||||||
setExcludedRowRendererIds(excludedRowRenderers.map((rowRenderer) => rowRenderer.id));
|
|
||||||
},
|
|
||||||
[setExcludedRowRendererIds]
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectionValue = useMemo(
|
|
||||||
() => ({
|
|
||||||
selectable: handleSelectable,
|
|
||||||
onSelectionChange: handleSelectionChange,
|
|
||||||
initialSelected: notExcludedRowRenderers,
|
|
||||||
}),
|
|
||||||
[handleSelectable, handleSelectionChange, notExcludedRowRenderers]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledEuiInMemoryTable
|
|
||||||
ref={ref}
|
|
||||||
items={renderers}
|
|
||||||
itemId="id"
|
|
||||||
columns={columns}
|
|
||||||
search={search}
|
|
||||||
sorting={initialSorting}
|
|
||||||
isSelectable={true}
|
|
||||||
selection={selectionValue}
|
|
||||||
/>
|
/>
|
||||||
);
|
),
|
||||||
}
|
[excludedRowRendererIds, handleNameClick]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
name: '',
|
||||||
|
sortable: false,
|
||||||
|
width: '32px',
|
||||||
|
render: idColumnRenderCallback,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
name: 'Name',
|
||||||
|
sortable: true,
|
||||||
|
width: '10%',
|
||||||
|
render: nameColumnRenderCallback,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
name: 'Description',
|
||||||
|
width: '25%',
|
||||||
|
render: (description: React.ReactNode) => description,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'example',
|
||||||
|
name: 'Example',
|
||||||
|
width: '65%',
|
||||||
|
render: ExampleWrapperComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'searchableDescription',
|
||||||
|
name: 'Searchable Description',
|
||||||
|
sortable: false,
|
||||||
|
width: '0px',
|
||||||
|
render: renderSearchableDescriptionNoop,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[idColumnRenderCallback, nameColumnRenderCallback]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledEuiInMemoryTable
|
||||||
|
items={renderers}
|
||||||
|
itemId="id"
|
||||||
|
columns={columns}
|
||||||
|
search={search}
|
||||||
|
sorting={initialSorting}
|
||||||
|
isSelectable={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
RowRenderersBrowserComponent.displayName = 'RowRenderersBrowserComponent';
|
RowRenderersBrowserComponent.displayName = 'RowRenderersBrowserComponent';
|
||||||
|
|
||||||
|
|
|
@ -474,6 +474,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
tabType="query"
|
||||||
timelineId="test"
|
timelineId="test"
|
||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -4,19 +4,23 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover } from '@elastic/eui';
|
||||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { Draggable } from 'react-beautiful-dnd';
|
import { Draggable } from 'react-beautiful-dnd';
|
||||||
import { Resizable, ResizeCallback } from 're-resizable';
|
import { Resizable, ResizeCallback } from 're-resizable';
|
||||||
import deepEqual from 'fast-deep-equal';
|
import deepEqual from 'fast-deep-equal';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { useDraggableKeyboardWrapper } from '../../../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook';
|
import { useDraggableKeyboardWrapper } from '../../../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook';
|
||||||
import {
|
import {
|
||||||
DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME,
|
DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME,
|
||||||
getDraggableFieldId,
|
getDraggableFieldId,
|
||||||
} from '../../../../../common/components/drag_and_drop/helpers';
|
} from '../../../../../common/components/drag_and_drop/helpers';
|
||||||
|
import { TimelineTabs } from '../../../../../../common/types/timeline';
|
||||||
import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
|
import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
|
||||||
import { OnFilterChange } from '../../events';
|
import { OnFilterChange } from '../../events';
|
||||||
|
import { Direction } from '../../../../../graphql/types';
|
||||||
import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers';
|
import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers';
|
||||||
import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles';
|
import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles';
|
||||||
import { Sort } from '../sort';
|
import { Sort } from '../sort';
|
||||||
|
@ -24,6 +28,25 @@ import { Sort } from '../sort';
|
||||||
import { Header } from './header';
|
import { Header } from './header';
|
||||||
import { timelineActions } from '../../../../store/timeline';
|
import { timelineActions } from '../../../../store/timeline';
|
||||||
|
|
||||||
|
import * as i18n from './translations';
|
||||||
|
|
||||||
|
const ContextMenu = styled(EuiContextMenu)`
|
||||||
|
width: 115px;
|
||||||
|
|
||||||
|
& .euiContextMenuItem {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
width: 115px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const PopoverContainer = styled.div<{ $width: number }>`
|
||||||
|
& .euiPopover__anchor {
|
||||||
|
padding-right: 8px;
|
||||||
|
width: ${({ $width }) => $width}px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const RESIZABLE_ENABLE = { right: true };
|
const RESIZABLE_ENABLE = { right: true };
|
||||||
|
|
||||||
interface ColumneHeaderProps {
|
interface ColumneHeaderProps {
|
||||||
|
@ -32,6 +55,7 @@ interface ColumneHeaderProps {
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
onFilterChange?: OnFilterChange;
|
onFilterChange?: OnFilterChange;
|
||||||
sort: Sort[];
|
sort: Sort[];
|
||||||
|
tabType: TimelineTabs;
|
||||||
timelineId: string;
|
timelineId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,10 +66,11 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
|
||||||
isDragging,
|
isDragging,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
sort,
|
sort,
|
||||||
|
tabType,
|
||||||
}) => {
|
}) => {
|
||||||
const keyboardHandlerRef = useRef<HTMLDivElement | null>(null);
|
const keyboardHandlerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [, setClosePopOverTrigger] = useState(false);
|
const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState<boolean>(false);
|
||||||
const [, setHoverActionsOwnFocus] = useState<boolean>(false);
|
const restoreFocus = useCallback(() => keyboardHandlerRef.current?.focus(), []);
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const resizableSize = useMemo(
|
const resizableSize = useMemo(
|
||||||
|
@ -84,10 +109,93 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
|
||||||
const draggableId = useMemo(
|
const draggableId = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getDraggableFieldId({
|
getDraggableFieldId({
|
||||||
contextId: `timeline-column-headers-${timelineId}`,
|
contextId: `timeline-column-headers-${tabType}-${timelineId}`,
|
||||||
fieldId: header.id,
|
fieldId: header.id,
|
||||||
}),
|
}),
|
||||||
[timelineId, header.id]
|
[tabType, timelineId, header.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onColumnSort = useCallback(
|
||||||
|
(sortDirection: Direction) => {
|
||||||
|
const columnId = header.id;
|
||||||
|
const headerIndex = sort.findIndex((col) => col.columnId === columnId);
|
||||||
|
const newSort =
|
||||||
|
headerIndex === -1
|
||||||
|
? [
|
||||||
|
...sort,
|
||||||
|
{
|
||||||
|
columnId,
|
||||||
|
columnType: `${header.type}`,
|
||||||
|
sortDirection,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
...sort.slice(0, headerIndex),
|
||||||
|
{
|
||||||
|
columnId,
|
||||||
|
columnType: `${header.type}`,
|
||||||
|
sortDirection,
|
||||||
|
},
|
||||||
|
...sort.slice(headerIndex + 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
timelineActions.updateSort({
|
||||||
|
id: timelineId,
|
||||||
|
sort: newSort,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch, header, sort, timelineId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClosePopOverTrigger = useCallback(() => {
|
||||||
|
setHoverActionsOwnFocus(false);
|
||||||
|
restoreFocus();
|
||||||
|
}, [restoreFocus]);
|
||||||
|
|
||||||
|
const panels: EuiContextMenuPanelDescriptor[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
icon: <EuiIcon type="eyeClosed" size="s" />,
|
||||||
|
name: i18n.HIDE_COLUMN,
|
||||||
|
onClick: () => {
|
||||||
|
dispatch(timelineActions.removeColumn({ id: timelineId, columnId: header.id }));
|
||||||
|
handleClosePopOverTrigger();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
disabled: !header.aggregatable,
|
||||||
|
icon: <EuiIcon type="sortUp" size="s" />,
|
||||||
|
name: i18n.SORT_AZ,
|
||||||
|
onClick: () => {
|
||||||
|
onColumnSort(Direction.asc);
|
||||||
|
handleClosePopOverTrigger();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
disabled: !header.aggregatable,
|
||||||
|
icon: <EuiIcon type="sortDown" size="s" />,
|
||||||
|
name: i18n.SORT_ZA,
|
||||||
|
onClick: () => {
|
||||||
|
onColumnSort(Direction.desc);
|
||||||
|
handleClosePopOverTrigger();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[dispatch, handleClosePopOverTrigger, header.aggregatable, header.id, onColumnSort, timelineId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const headerButton = useMemo(
|
||||||
|
() => (
|
||||||
|
<Header timelineId={timelineId} header={header} onFilterChange={onFilterChange} sort={sort} />
|
||||||
|
),
|
||||||
|
[header, onFilterChange, sort, timelineId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const DraggableContent = useCallback(
|
const DraggableContent = useCallback(
|
||||||
|
@ -99,26 +207,28 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
|
||||||
ref={dragProvided.innerRef}
|
ref={dragProvided.innerRef}
|
||||||
>
|
>
|
||||||
<EventsThContent>
|
<EventsThContent>
|
||||||
<Header
|
<PopoverContainer $width={header.width}>
|
||||||
timelineId={timelineId}
|
<EuiPopover
|
||||||
header={header}
|
anchorPosition="downLeft"
|
||||||
onFilterChange={onFilterChange}
|
button={headerButton}
|
||||||
sort={sort}
|
closePopover={handleClosePopOverTrigger}
|
||||||
/>
|
isOpen={hoverActionsOwnFocus}
|
||||||
|
ownFocus
|
||||||
|
panelPaddingSize="none"
|
||||||
|
>
|
||||||
|
<ContextMenu initialPanelId={0} panels={panels} />
|
||||||
|
</EuiPopover>
|
||||||
|
</PopoverContainer>
|
||||||
</EventsThContent>
|
</EventsThContent>
|
||||||
</EventsTh>
|
</EventsTh>
|
||||||
),
|
),
|
||||||
[header, onFilterChange, sort, timelineId]
|
[handleClosePopOverTrigger, headerButton, header.width, hoverActionsOwnFocus, panels]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onFocus = useCallback(() => {
|
const onFocus = useCallback(() => {
|
||||||
keyboardHandlerRef.current?.focus();
|
keyboardHandlerRef.current?.focus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleClosePopOverTrigger = useCallback(() => {
|
|
||||||
setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const openPopover = useCallback(() => {
|
const openPopover = useCallback(() => {
|
||||||
setHoverActionsOwnFocus(true);
|
setHoverActionsOwnFocus(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -131,6 +241,15 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
|
||||||
openPopover,
|
openPopover,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const keyDownHandler = useCallback(
|
||||||
|
(keyboardEvent: React.KeyboardEvent) => {
|
||||||
|
if (!hoverActionsOwnFocus) {
|
||||||
|
onKeyDown(keyboardEvent);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[hoverActionsOwnFocus, onKeyDown]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Resizable
|
<Resizable
|
||||||
enable={RESIZABLE_ENABLE}
|
enable={RESIZABLE_ENABLE}
|
||||||
|
@ -147,7 +266,7 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
|
||||||
data-test-subj="draggableWrapperKeyboardHandler"
|
data-test-subj="draggableWrapperKeyboardHandler"
|
||||||
onClick={onFocus}
|
onClick={onFocus}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={keyDownHandler}
|
||||||
ref={keyboardHandlerRef}
|
ref={keyboardHandlerRef}
|
||||||
role="columnheader"
|
role="columnheader"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
@ -171,6 +290,7 @@ export const ColumnHeader = React.memo(
|
||||||
ColumnHeaderComponent,
|
ColumnHeaderComponent,
|
||||||
(prevProps, nextProps) =>
|
(prevProps, nextProps) =>
|
||||||
prevProps.draggableIndex === nextProps.draggableIndex &&
|
prevProps.draggableIndex === nextProps.draggableIndex &&
|
||||||
|
prevProps.tabType === nextProps.tabType &&
|
||||||
prevProps.timelineId === nextProps.timelineId &&
|
prevProps.timelineId === nextProps.timelineId &&
|
||||||
prevProps.isDragging === nextProps.isDragging &&
|
prevProps.isDragging === nextProps.isDragging &&
|
||||||
prevProps.onFilterChange === nextProps.onFilterChange &&
|
prevProps.onFilterChange === nextProps.onFilterChange &&
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended
|
||||||
import { ColumnHeadersComponent } from '.';
|
import { ColumnHeadersComponent } from '.';
|
||||||
import { cloneDeep } from 'lodash/fp';
|
import { cloneDeep } from 'lodash/fp';
|
||||||
import { timelineActions } from '../../../../store/timeline';
|
import { timelineActions } from '../../../../store/timeline';
|
||||||
|
import { TimelineTabs } from '../../../../../../common/types/timeline';
|
||||||
|
|
||||||
const mockDispatch = jest.fn();
|
const mockDispatch = jest.fn();
|
||||||
jest.mock('react-redux', () => {
|
jest.mock('react-redux', () => {
|
||||||
|
@ -55,6 +56,7 @@ describe('ColumnHeaders', () => {
|
||||||
showEventsSelect={false}
|
showEventsSelect={false}
|
||||||
showSelectAllCheckbox={false}
|
showSelectAllCheckbox={false}
|
||||||
sort={sort}
|
sort={sort}
|
||||||
|
tabType={TimelineTabs.query}
|
||||||
timelineId={timelineId}
|
timelineId={timelineId}
|
||||||
/>
|
/>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -74,6 +76,7 @@ describe('ColumnHeaders', () => {
|
||||||
showEventsSelect={false}
|
showEventsSelect={false}
|
||||||
showSelectAllCheckbox={false}
|
showSelectAllCheckbox={false}
|
||||||
sort={sort}
|
sort={sort}
|
||||||
|
tabType={TimelineTabs.query}
|
||||||
timelineId={timelineId}
|
timelineId={timelineId}
|
||||||
/>
|
/>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -94,6 +97,7 @@ describe('ColumnHeaders', () => {
|
||||||
showEventsSelect={false}
|
showEventsSelect={false}
|
||||||
showSelectAllCheckbox={false}
|
showSelectAllCheckbox={false}
|
||||||
sort={sort}
|
sort={sort}
|
||||||
|
tabType={TimelineTabs.query}
|
||||||
timelineId={timelineId}
|
timelineId={timelineId}
|
||||||
/>
|
/>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -152,6 +156,7 @@ describe('ColumnHeaders', () => {
|
||||||
showEventsSelect={false}
|
showEventsSelect={false}
|
||||||
showSelectAllCheckbox={false}
|
showSelectAllCheckbox={false}
|
||||||
sort={mockSort}
|
sort={mockSort}
|
||||||
|
tabType={TimelineTabs.query}
|
||||||
timelineId={timelineId}
|
timelineId={timelineId}
|
||||||
/>
|
/>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -193,6 +198,7 @@ describe('ColumnHeaders', () => {
|
||||||
showEventsSelect={false}
|
showEventsSelect={false}
|
||||||
showSelectAllCheckbox={false}
|
showSelectAllCheckbox={false}
|
||||||
sort={mockSort}
|
sort={mockSort}
|
||||||
|
tabType={TimelineTabs.query}
|
||||||
timelineId={timelineId}
|
timelineId={timelineId}
|
||||||
/>
|
/>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
@ -229,6 +235,7 @@ describe('ColumnHeaders', () => {
|
||||||
showEventsSelect={false}
|
showEventsSelect={false}
|
||||||
showSelectAllCheckbox={false}
|
showSelectAllCheckbox={false}
|
||||||
sort={mockSort}
|
sort={mockSort}
|
||||||
|
tabType={TimelineTabs.query}
|
||||||
timelineId={timelineId}
|
timelineId={timelineId}
|
||||||
/>
|
/>
|
||||||
</TestProviders>
|
</TestProviders>
|
||||||
|
|
|
@ -31,7 +31,7 @@ import {
|
||||||
useGlobalFullScreen,
|
useGlobalFullScreen,
|
||||||
useTimelineFullScreen,
|
useTimelineFullScreen,
|
||||||
} from '../../../../../common/containers/use_full_screen';
|
} from '../../../../../common/containers/use_full_screen';
|
||||||
import { TimelineId } from '../../../../../../common/types/timeline';
|
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
|
||||||
import { OnSelectAll } from '../../events';
|
import { OnSelectAll } from '../../events';
|
||||||
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
|
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
|
||||||
import { StatefulFieldsBrowser } from '../../../fields_browser';
|
import { StatefulFieldsBrowser } from '../../../fields_browser';
|
||||||
|
@ -76,6 +76,7 @@ interface Props {
|
||||||
showEventsSelect: boolean;
|
showEventsSelect: boolean;
|
||||||
showSelectAllCheckbox: boolean;
|
showSelectAllCheckbox: boolean;
|
||||||
sort: Sort[];
|
sort: Sort[];
|
||||||
|
tabType: TimelineTabs;
|
||||||
timelineId: string;
|
timelineId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,6 +123,7 @@ export const ColumnHeadersComponent = ({
|
||||||
showEventsSelect,
|
showEventsSelect,
|
||||||
showSelectAllCheckbox,
|
showSelectAllCheckbox,
|
||||||
sort,
|
sort,
|
||||||
|
tabType,
|
||||||
timelineId,
|
timelineId,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
@ -186,9 +188,10 @@ export const ColumnHeadersComponent = ({
|
||||||
header={header}
|
header={header}
|
||||||
isDragging={draggingIndex === draggableIndex}
|
isDragging={draggingIndex === draggableIndex}
|
||||||
sort={sort}
|
sort={sort}
|
||||||
|
tabType={tabType}
|
||||||
/>
|
/>
|
||||||
)),
|
)),
|
||||||
[columnHeaders, timelineId, draggingIndex, sort]
|
[columnHeaders, timelineId, draggingIndex, sort, tabType]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fullScreen = useMemo(
|
const fullScreen = useMemo(
|
||||||
|
@ -335,7 +338,7 @@ export const ColumnHeadersComponent = ({
|
||||||
|
|
||||||
<Droppable
|
<Droppable
|
||||||
direction={'horizontal'}
|
direction={'horizontal'}
|
||||||
droppableId={`${droppableTimelineColumnsPrefix}${timelineId}`}
|
droppableId={`${droppableTimelineColumnsPrefix}-${tabType}.${timelineId}`}
|
||||||
isDropDisabled={false}
|
isDropDisabled={false}
|
||||||
type={DRAG_TYPE_FIELD}
|
type={DRAG_TYPE_FIELD}
|
||||||
renderClone={renderClone}
|
renderClone={renderClone}
|
||||||
|
|
|
@ -22,10 +22,22 @@ export const FULL_SCREEN = i18n.translate('xpack.securitySolution.timeline.fullS
|
||||||
defaultMessage: 'Full screen',
|
defaultMessage: 'Full screen',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const HIDE_COLUMN = i18n.translate('xpack.securitySolution.timeline.hideColumnLabel', {
|
||||||
|
defaultMessage: 'Hide column',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SORT_AZ = i18n.translate('xpack.securitySolution.timeline.sortAZLabel', {
|
||||||
|
defaultMessage: 'Sort A-Z',
|
||||||
|
});
|
||||||
|
|
||||||
export const SORT_FIELDS = i18n.translate('xpack.securitySolution.timeline.sortFieldsButton', {
|
export const SORT_FIELDS = i18n.translate('xpack.securitySolution.timeline.sortFieldsButton', {
|
||||||
defaultMessage: 'Sort fields',
|
defaultMessage: 'Sort fields',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const SORT_ZA = i18n.translate('xpack.securitySolution.timeline.sortZALabel', {
|
||||||
|
defaultMessage: 'Sort Z-A',
|
||||||
|
});
|
||||||
|
|
||||||
export const TYPE = i18n.translate('xpack.securitySolution.timeline.typeTooltip', {
|
export const TYPE = i18n.translate('xpack.securitySolution.timeline.typeTooltip', {
|
||||||
defaultMessage: 'Type',
|
defaultMessage: 'Type',
|
||||||
});
|
});
|
||||||
|
|
|
@ -195,6 +195,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
||||||
showEventsSelect={false}
|
showEventsSelect={false}
|
||||||
showSelectAllCheckbox={showCheckboxes}
|
showSelectAllCheckbox={showCheckboxes}
|
||||||
sort={sort}
|
sort={sort}
|
||||||
|
tabType={tabType}
|
||||||
timelineId={id}
|
timelineId={id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,11 @@ exports[`Provider rendering renders correctly against snapshot 1`] = `
|
||||||
field="name"
|
field="name"
|
||||||
isEnabled={true}
|
isEnabled={true}
|
||||||
isExcluded={false}
|
isExcluded={false}
|
||||||
|
isPopoverOpen={false}
|
||||||
kqlQuery=""
|
kqlQuery=""
|
||||||
operator=":"
|
operator=":"
|
||||||
providerId="id-Provider 1"
|
providerId="id-Provider 1"
|
||||||
|
setIsPopoverOpen={[Function]}
|
||||||
toggleEnabledProvider={[Function]}
|
toggleEnabledProvider={[Function]}
|
||||||
toggleExcludedProvider={[Function]}
|
toggleExcludedProvider={[Function]}
|
||||||
toggleTypeProvider={[Function]}
|
toggleTypeProvider={[Function]}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { noop } from 'lodash/fp';
|
import { noop } from 'lodash/fp';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { DataProvider, DataProviderType, IS_OPERATOR } from './data_provider';
|
import { DataProvider, DataProviderType, IS_OPERATOR } from './data_provider';
|
||||||
import { ProviderItemBadge } from './provider_item_badge';
|
import { ProviderItemBadge } from './provider_item_badge';
|
||||||
|
@ -14,21 +14,27 @@ interface OwnProps {
|
||||||
dataProvider: DataProvider;
|
dataProvider: DataProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Provider = React.memo<OwnProps>(({ dataProvider }) => (
|
export const Provider = React.memo<OwnProps>(({ dataProvider }) => {
|
||||||
<ProviderItemBadge
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||||
deleteProvider={noop}
|
|
||||||
field={dataProvider.queryMatch.displayField || dataProvider.queryMatch.field}
|
return (
|
||||||
kqlQuery={dataProvider.kqlQuery}
|
<ProviderItemBadge
|
||||||
isEnabled={dataProvider.enabled}
|
deleteProvider={noop}
|
||||||
isExcluded={dataProvider.excluded}
|
field={dataProvider.queryMatch.displayField || dataProvider.queryMatch.field}
|
||||||
providerId={dataProvider.id}
|
kqlQuery={dataProvider.kqlQuery}
|
||||||
toggleExcludedProvider={noop}
|
isEnabled={dataProvider.enabled}
|
||||||
toggleEnabledProvider={noop}
|
isExcluded={dataProvider.excluded}
|
||||||
toggleTypeProvider={noop}
|
providerId={dataProvider.id}
|
||||||
val={dataProvider.queryMatch.displayValue || dataProvider.queryMatch.value}
|
isPopoverOpen={isPopoverOpen}
|
||||||
operator={dataProvider.queryMatch.operator || IS_OPERATOR}
|
setIsPopoverOpen={setIsPopoverOpen}
|
||||||
type={dataProvider.type || DataProviderType.default}
|
toggleExcludedProvider={noop}
|
||||||
/>
|
toggleEnabledProvider={noop}
|
||||||
));
|
toggleTypeProvider={noop}
|
||||||
|
val={dataProvider.queryMatch.displayValue || dataProvider.queryMatch.value}
|
||||||
|
operator={dataProvider.queryMatch.operator || IS_OPERATOR}
|
||||||
|
type={dataProvider.type || DataProviderType.default}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
Provider.displayName = 'Provider';
|
Provider.displayName = 'Provider';
|
||||||
|
|
|
@ -231,6 +231,7 @@ export class ProviderItemActions extends React.PureComponent<OwnProps> {
|
||||||
button={button}
|
button={button}
|
||||||
anchorPosition="downCenter"
|
anchorPosition="downCenter"
|
||||||
panelPaddingSize="none"
|
panelPaddingSize="none"
|
||||||
|
ownFocus={true}
|
||||||
>
|
>
|
||||||
<div style={{ userSelect: 'none' }}>
|
<div style={{ userSelect: 'none' }}>
|
||||||
<EuiContextMenu initialPanelId={0} panels={panelTree} data-test-subj="providerActions" />
|
<EuiContextMenu initialPanelId={0} panels={panelTree} data-test-subj="providerActions" />
|
||||||
|
|
|
@ -28,10 +28,12 @@ interface ProviderItemBadgeProps {
|
||||||
kqlQuery: string;
|
kqlQuery: string;
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
isExcluded: boolean;
|
isExcluded: boolean;
|
||||||
|
isPopoverOpen: boolean;
|
||||||
onDataProviderEdited?: OnDataProviderEdited;
|
onDataProviderEdited?: OnDataProviderEdited;
|
||||||
operator: QueryOperator;
|
operator: QueryOperator;
|
||||||
providerId: string;
|
providerId: string;
|
||||||
register?: DataProvidersAnd;
|
register?: DataProvidersAnd;
|
||||||
|
setIsPopoverOpen: (isPopoverOpen: boolean) => void;
|
||||||
timelineId?: string;
|
timelineId?: string;
|
||||||
toggleEnabledProvider: () => void;
|
toggleEnabledProvider: () => void;
|
||||||
toggleExcludedProvider: () => void;
|
toggleExcludedProvider: () => void;
|
||||||
|
@ -50,10 +52,12 @@ export const ProviderItemBadge = React.memo<ProviderItemBadgeProps>(
|
||||||
kqlQuery,
|
kqlQuery,
|
||||||
isEnabled,
|
isEnabled,
|
||||||
isExcluded,
|
isExcluded,
|
||||||
|
isPopoverOpen,
|
||||||
onDataProviderEdited,
|
onDataProviderEdited,
|
||||||
operator,
|
operator,
|
||||||
providerId,
|
providerId,
|
||||||
register,
|
register,
|
||||||
|
setIsPopoverOpen,
|
||||||
timelineId,
|
timelineId,
|
||||||
toggleEnabledProvider,
|
toggleEnabledProvider,
|
||||||
toggleExcludedProvider,
|
toggleExcludedProvider,
|
||||||
|
@ -75,16 +79,15 @@ export const ProviderItemBadge = React.memo<ProviderItemBadgeProps>(
|
||||||
getManageTimelineById,
|
getManageTimelineById,
|
||||||
timelineId,
|
timelineId,
|
||||||
]);
|
]);
|
||||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
|
||||||
|
|
||||||
const togglePopover = useCallback(() => {
|
const togglePopover = useCallback(() => {
|
||||||
setIsPopoverOpen(!isPopoverOpen);
|
setIsPopoverOpen(!isPopoverOpen);
|
||||||
}, [isPopoverOpen]);
|
}, [isPopoverOpen, setIsPopoverOpen]);
|
||||||
|
|
||||||
const closePopover = useCallback(() => {
|
const closePopover = useCallback(() => {
|
||||||
setIsPopoverOpen(false);
|
setIsPopoverOpen(false);
|
||||||
wrapperRef?.current?.focus();
|
wrapperRef?.current?.focus();
|
||||||
}, [wrapperRef]);
|
}, [wrapperRef, setIsPopoverOpen]);
|
||||||
|
|
||||||
const onToggleEnabledProvider = useCallback(() => {
|
const onToggleEnabledProvider = useCallback(() => {
|
||||||
toggleEnabledProvider();
|
toggleEnabledProvider();
|
||||||
|
|
|
@ -155,7 +155,7 @@ interface DataProvidersGroupItem extends Omit<Props, 'dataProviders'> {
|
||||||
export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
|
export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
|
||||||
({ browserFields, group, groupIndex, dataProvider, index, timelineId }) => {
|
({ browserFields, group, groupIndex, dataProvider, index, timelineId }) => {
|
||||||
const keyboardHandlerRef = useRef<HTMLDivElement | null>(null);
|
const keyboardHandlerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [, setHoverActionsOwnFocus] = useState<boolean>(false);
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||||
const [, setClosePopOverTrigger] = useState(false);
|
const [, setClosePopOverTrigger] = useState(false);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
@ -240,7 +240,7 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const openPopover = useCallback(() => {
|
const openPopover = useCallback(() => {
|
||||||
setHoverActionsOwnFocus(true);
|
setIsPopoverOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
|
const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
|
||||||
|
@ -251,6 +251,15 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
|
||||||
openPopover,
|
openPopover,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const keyDownHandler = useCallback(
|
||||||
|
(keyboardEvent: React.KeyboardEvent<Element>) => {
|
||||||
|
if (keyboardHandlerRef.current === document.activeElement) {
|
||||||
|
onKeyDown(keyboardEvent);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onKeyDown]
|
||||||
|
);
|
||||||
|
|
||||||
const DraggableContent = useCallback(
|
const DraggableContent = useCallback(
|
||||||
(provided, snapshot) => (
|
(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
|
@ -275,6 +284,7 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
|
||||||
kqlQuery={index > 0 ? dataProvider.kqlQuery : group[0].kqlQuery}
|
kqlQuery={index > 0 ? dataProvider.kqlQuery : group[0].kqlQuery}
|
||||||
isEnabled={index > 0 ? dataProvider.enabled : group[0].enabled}
|
isEnabled={index > 0 ? dataProvider.enabled : group[0].enabled}
|
||||||
isExcluded={index > 0 ? dataProvider.excluded : group[0].excluded}
|
isExcluded={index > 0 ? dataProvider.excluded : group[0].excluded}
|
||||||
|
isPopoverOpen={isPopoverOpen}
|
||||||
onDataProviderEdited={handleDataProviderEdited}
|
onDataProviderEdited={handleDataProviderEdited}
|
||||||
operator={
|
operator={
|
||||||
index > 0
|
index > 0
|
||||||
|
@ -284,6 +294,7 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
|
||||||
register={dataProvider}
|
register={dataProvider}
|
||||||
providerId={index > 0 ? group[0].id : dataProvider.id}
|
providerId={index > 0 ? group[0].id : dataProvider.id}
|
||||||
timelineId={timelineId}
|
timelineId={timelineId}
|
||||||
|
setIsPopoverOpen={setIsPopoverOpen}
|
||||||
toggleEnabledProvider={handleToggleEnabledProvider}
|
toggleEnabledProvider={handleToggleEnabledProvider}
|
||||||
toggleExcludedProvider={handleToggleExcludedProvider}
|
toggleExcludedProvider={handleToggleExcludedProvider}
|
||||||
toggleTypeProvider={handleToggleTypeProvider}
|
toggleTypeProvider={handleToggleTypeProvider}
|
||||||
|
@ -315,7 +326,9 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
|
||||||
handleToggleExcludedProvider,
|
handleToggleExcludedProvider,
|
||||||
handleToggleTypeProvider,
|
handleToggleTypeProvider,
|
||||||
index,
|
index,
|
||||||
|
isPopoverOpen,
|
||||||
keyboardHandlerRef,
|
keyboardHandlerRef,
|
||||||
|
setIsPopoverOpen,
|
||||||
timelineId,
|
timelineId,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -326,7 +339,7 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
|
||||||
data-test-subj="draggableWrapperKeyboardHandler"
|
data-test-subj="draggableWrapperKeyboardHandler"
|
||||||
onClick={onFocus}
|
onClick={onFocus}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={keyDownHandler}
|
||||||
ref={keyboardHandlerRef}
|
ref={keyboardHandlerRef}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
|
|
@ -242,3 +242,40 @@ export const onTimelineTabKeyPressed = ({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ACTIVE_TIMELINE_BUTTON_CLASS_NAME = 'active-timeline-button';
|
||||||
|
export const FLYOUT_BUTTON_BAR_CLASS_NAME = 'timeline-flyout-button-bar';
|
||||||
|
export const FLYOUT_BUTTON_CLASS_NAME = 'timeline-flyout-button';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function focuses the active timeline button on the next tick. Focus
|
||||||
|
* is updated on the next tick because this function is typically
|
||||||
|
* invoked in `onClick` handlers that also dispatch Redux actions (that
|
||||||
|
* in-turn update focus states).
|
||||||
|
*/
|
||||||
|
export const focusActiveTimelineButton = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
document
|
||||||
|
.querySelector<HTMLButtonElement>(
|
||||||
|
`div.${FLYOUT_BUTTON_BAR_CLASS_NAME} .${ACTIVE_TIMELINE_BUTTON_CLASS_NAME}`
|
||||||
|
)
|
||||||
|
?.focus();
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focuses the utility bar action contained by the provided `containerElement`
|
||||||
|
* when a valid container is provided
|
||||||
|
*/
|
||||||
|
export const focusUtilityBarAction = (containerElement: HTMLElement | null) => {
|
||||||
|
containerElement
|
||||||
|
?.querySelector<HTMLButtonElement>('div.siemUtilityBar__action:last-of-type button')
|
||||||
|
?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets keyboard focus on the page
|
||||||
|
*/
|
||||||
|
export const resetKeyboardFocus = () => {
|
||||||
|
document.querySelector<HTMLAnchorElement>('header.headerGlobalNav a.euiHeaderLogo')?.focus();
|
||||||
|
};
|
||||||
|
|
|
@ -432,6 +432,7 @@ export const EventsHeadingHandle = styled.div.attrs(({ className = '' }) => ({
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const EventsLoading = styled(EuiLoadingSpinner)`
|
export const EventsLoading = styled(EuiLoadingSpinner)`
|
||||||
|
margin: 0 2px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue