[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_SYSTEM_CATEGORIES_COUNT,
|
||||
} from '../screens/fields_browser';
|
||||
import { TIMELINE_FIELDS_BUTTON } from '../screens/timeline';
|
||||
import { cleanKibana } from '../tasks/common';
|
||||
|
||||
import {
|
||||
|
@ -182,5 +183,19 @@ describe('Fields Browser', () => {
|
|||
|
||||
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_EMPTY,
|
||||
TIMELINE_DROPPED_DATA_PROVIDERS,
|
||||
TIMELINE_DATA_PROVIDERS_ACTION_MENU,
|
||||
} from '../screens/timeline';
|
||||
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', () => {
|
||||
dragFirstHostToTimeline();
|
||||
|
||||
|
|
|
@ -4,12 +4,22 @@
|
|||
* 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 { dragFirstHostToTimeline, waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts';
|
||||
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';
|
||||
|
||||
|
@ -26,6 +36,34 @@ describe('timeline flyout button', () => {
|
|||
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', () => {
|
||||
dragFirstHostToTimeline();
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* 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 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_ACTION_MENU = '[data-test-subj="providerActions"]';
|
||||
|
||||
export const TIMELINE_DATA_PROVIDERS_EMPTY =
|
||||
'[data-test-subj="dataProviders"] [data-test-subj="empty"]';
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
CLOSE_TIMELINE_BUTTON,
|
||||
MAIN_PAGE,
|
||||
TIMELINE_TOGGLE_BUTTON,
|
||||
TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON,
|
||||
|
@ -18,6 +19,10 @@ export const closeTimelineUsingToggle = () => {
|
|||
cy.get(TIMELINE_TOGGLE_BUTTON).filter(':visible').click();
|
||||
};
|
||||
|
||||
export const closeTimelineUsingCloseButton = () => {
|
||||
cy.get(CLOSE_TIMELINE_BUTTON).filter(':visible').click();
|
||||
};
|
||||
|
||||
export const openTimelineIfClosed = () =>
|
||||
cy.get(MAIN_PAGE).then(($page) => {
|
||||
if ($page.find(TIMELINE_BOTTOM_BAR_TOGGLE_BUTTON).length === 1) {
|
||||
|
|
|
@ -220,7 +220,7 @@ const AlertsUtilityBarComponent: React.FC<AlertsUtilityBarProps> = ({
|
|||
disabled={areEventsLoading}
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
ownFocus={false}
|
||||
ownFocus={true}
|
||||
popoverContent={UtilityBarAdditionalFiltersContent}
|
||||
>
|
||||
{i18n.ADDITIONAL_FILTERS_ACTIONS}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { EuiSpacer, EuiWindowEvent } from '@elastic/eui';
|
||||
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 { useHistory } from 'react-router-dom';
|
||||
|
||||
|
@ -14,6 +14,7 @@ import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/h
|
|||
import { SecurityPageName } from '../../../app/types';
|
||||
import { TimelineId } from '../../../../common/types/timeline';
|
||||
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
||||
import { isTab } from '../../../common/components/accessibility/helpers';
|
||||
import { UpdateDateRange } from '../../../common/components/charts/common';
|
||||
import { FiltersGlobal } from '../../../common/components/filters_global';
|
||||
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 { useGlobalFullScreen } from '../../../common/containers/use_full_screen';
|
||||
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 { timelineDefaults } from '../../../timelines/store/timeline/defaults';
|
||||
import { buildShowBuildingBlockFilter } from '../../components/alerts_table/default_config';
|
||||
|
@ -48,6 +54,7 @@ import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
|||
|
||||
const DetectionEnginePageComponent = () => {
|
||||
const dispatch = useDispatch();
|
||||
const containerElement = useRef<HTMLDivElement | null>(null);
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const graphEventId = useShallowEqualSelector(
|
||||
(state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).graphEventId
|
||||
|
@ -128,6 +135,28 @@ const DetectionEnginePageComponent = () => {
|
|||
|
||||
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) {
|
||||
return (
|
||||
<WrapperPage>
|
||||
|
@ -154,7 +183,7 @@ const DetectionEnginePageComponent = () => {
|
|||
{hasEncryptionKey != null && !hasEncryptionKey && <NoApiIntegrationKeyCallOut />}
|
||||
<ReadOnlyAlertsCallOut />
|
||||
{indicesExist ? (
|
||||
<>
|
||||
<div onKeyDown={onKeyDown} ref={containerElement}>
|
||||
<EuiWindowEvent event="resize" handler={noop} />
|
||||
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
|
||||
<SiemSearchBar id="global" indexPattern={indexPattern} />
|
||||
|
@ -211,7 +240,7 @@ const DetectionEnginePageComponent = () => {
|
|||
to={to}
|
||||
/>
|
||||
</WrapperPage>
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
<WrapperPage>
|
||||
<DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} />
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
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 { useDispatch } from 'react-redux';
|
||||
|
||||
|
@ -81,7 +81,12 @@ import { useGlobalFullScreen } from '../../../../../common/containers/use_full_s
|
|||
import { Display } from '../../../../../hosts/pages/display';
|
||||
import { ExceptionListTypeEnum, ExceptionListIdentifiers } from '../../../../../shared_imports';
|
||||
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 { timelineDefaults } from '../../../../../timelines/store/timeline/defaults';
|
||||
import { useSourcererScope } from '../../../../../common/containers/sourcerer';
|
||||
|
@ -95,6 +100,7 @@ import {
|
|||
import * as detectionI18n from '../../translations';
|
||||
import * as ruleI18n from '../translations';
|
||||
import * as i18n from './translations';
|
||||
import { isTab } from '../../../../../common/components/accessibility/helpers';
|
||||
|
||||
enum RuleDetailTabs {
|
||||
alerts = 'alerts',
|
||||
|
@ -127,6 +133,7 @@ const getRuleDetailsTabs = (rule: Rule | null) => {
|
|||
|
||||
const RuleDetailsPageComponent = () => {
|
||||
const dispatch = useDispatch();
|
||||
const containerElement = useRef<HTMLDivElement | null>(null);
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const graphEventId = useShallowEqualSelector(
|
||||
(state) =>
|
||||
|
@ -408,6 +415,28 @@ const RuleDetailsPageComponent = () => {
|
|||
}
|
||||
}, [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 (
|
||||
redirectToDetections(
|
||||
isSignalIndexExists,
|
||||
|
@ -430,7 +459,7 @@ const RuleDetailsPageComponent = () => {
|
|||
<ReadOnlyAlertsCallOut />
|
||||
<ReadOnlyRulesCallOut />
|
||||
{indicesExist ? (
|
||||
<>
|
||||
<div onKeyDown={onKeyDown} ref={containerElement}>
|
||||
<EuiWindowEvent event="resize" handler={noop} />
|
||||
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
|
||||
<SiemSearchBar id="global" indexPattern={indexPattern} />
|
||||
|
@ -588,7 +617,7 @@ const RuleDetailsPageComponent = () => {
|
|||
)}
|
||||
{ruleDetailTab === RuleDetailTabs.failures && <FailureHistory id={rule?.id} />}
|
||||
</WrapperPage>
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
<WrapperPage>
|
||||
<DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} />
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { EuiSpacer, EuiWindowEvent } from '@elastic/eui';
|
||||
import { noop } from 'lodash/fp';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
|
@ -40,7 +40,12 @@ import * as i18n from './translations';
|
|||
import { filterHostData } from './navigation';
|
||||
import { hostsModel } from '../store';
|
||||
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 { timelineDefaults } from '../../timelines/store/timeline/defaults';
|
||||
import { useSourcererScope } from '../../common/containers/sourcerer';
|
||||
|
@ -48,6 +53,7 @@ import { useDeepEqualSelector, useShallowEqualSelector } from '../../common/hook
|
|||
|
||||
const HostsComponent = () => {
|
||||
const dispatch = useDispatch();
|
||||
const containerElement = useRef<HTMLDivElement | null>(null);
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const graphEventId = useShallowEqualSelector(
|
||||
(state) =>
|
||||
|
@ -114,10 +120,34 @@ const HostsComponent = () => {
|
|||
[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 (
|
||||
<>
|
||||
{indicesExist ? (
|
||||
<>
|
||||
<div onKeyDown={onKeyDown} ref={containerElement}>
|
||||
<EuiWindowEvent event="resize" handler={noop} />
|
||||
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
|
||||
<SiemSearchBar indexPattern={indexPattern} id="global" />
|
||||
|
@ -167,7 +197,7 @@ const HostsComponent = () => {
|
|||
type={hostsModel.HostsType.page}
|
||||
/>
|
||||
</WrapperPage>
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
<WrapperPage>
|
||||
<HeaderPage border title={i18n.PAGE_TITLE} />
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { EuiSpacer, EuiWindowEvent } from '@elastic/eui';
|
||||
import { noop } from 'lodash/fp';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
|
@ -38,8 +38,13 @@ import { OverviewEmpty } from '../../overview/components/overview_empty';
|
|||
import * as i18n from './translations';
|
||||
import { NetworkComponentProps } from './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 { isTab } from '../../common/components/accessibility/helpers';
|
||||
import { TimelineId } from '../../../common/types/timeline';
|
||||
import { timelineDefaults } from '../../timelines/store/timeline/defaults';
|
||||
import { useSourcererScope } from '../../common/containers/sourcerer';
|
||||
|
@ -48,6 +53,7 @@ import { useDeepEqualSelector, useShallowEqualSelector } from '../../common/hook
|
|||
const NetworkComponent = React.memo<NetworkComponentProps>(
|
||||
({ networkPagePath, hasMlUserPermissions, capabilitiesFetched }) => {
|
||||
const dispatch = useDispatch();
|
||||
const containerElement = useRef<HTMLDivElement | null>(null);
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const graphEventId = useShallowEqualSelector(
|
||||
(state) =>
|
||||
|
@ -91,6 +97,31 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
|
|||
);
|
||||
|
||||
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({
|
||||
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
|
||||
indexPattern,
|
||||
|
@ -107,7 +138,7 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
|
|||
return (
|
||||
<>
|
||||
{indicesExist ? (
|
||||
<>
|
||||
<div onKeyDown={onKeyDown} ref={containerElement}>
|
||||
<EuiWindowEvent event="resize" handler={noop} />
|
||||
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
|
||||
<SiemSearchBar indexPattern={indexPattern} id="global" />
|
||||
|
@ -176,7 +207,7 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
|
|||
<NetworkRoutesLoading />
|
||||
)}
|
||||
</WrapperPage>
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
<WrapperPage>
|
||||
<HeaderPage border title={i18n.PAGE_TITLE} />
|
||||
|
|
|
@ -239,7 +239,7 @@ const FieldsBrowserComponent: React.FC<Props> = ({
|
|||
data-test-subj="header"
|
||||
filteredBrowserFields={filteredBrowserFields}
|
||||
isSearching={isSearching}
|
||||
onOutsideClick={onOutsideClick}
|
||||
onOutsideClick={closeAndRestoreFocus}
|
||||
onSearchInputChange={onInputChange}
|
||||
onUpdateColumns={onUpdateColumns}
|
||||
searchInput={searchInput}
|
||||
|
|
|
@ -55,6 +55,7 @@ const AddTimelineButtonComponent: React.FC<AddTimelineButtonComponentProps> = ({
|
|||
id="timelineSettingsPopover"
|
||||
isOpen={showActions}
|
||||
closePopover={onClosePopover}
|
||||
ownFocus
|
||||
repositionOnScroll
|
||||
>
|
||||
<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 { flattenIntoAndGroups } from '../../timeline/data_providers/helpers';
|
||||
import { DataProviders } from '../../timeline/data_providers';
|
||||
import { FLYOUT_BUTTON_BAR_CLASS_NAME, FLYOUT_BUTTON_CLASS_NAME } from '../../timeline/helpers';
|
||||
import { FlyoutHeaderPanel } from '../header';
|
||||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||
|
||||
export const FLYOUT_BUTTON_CLASS_NAME = 'timeline-flyout-button';
|
||||
|
||||
export const getBadgeCount = (dataProviders: DataProvider[]): number =>
|
||||
flattenIntoAndGroups(dataProviders).reduce((total, group) => total + group.length, 0);
|
||||
|
||||
|
@ -76,7 +75,11 @@ interface FlyoutBottomBarProps {
|
|||
export const FlyoutBottomBar = React.memo<FlyoutBottomBarProps>(
|
||||
({ activeTab, showDataproviders, timelineId }) => {
|
||||
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 || (!showDataproviders && activeTab !== TimelineTabs.query)) && (
|
||||
<DataProvidersPanel paddingSize="none">
|
||||
|
|
|
@ -13,6 +13,10 @@ import { FormattedRelative } from '@kbn/i18n/react';
|
|||
|
||||
import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline';
|
||||
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 { timelineActions } from '../../../store/timeline';
|
||||
import * as i18n from './translations';
|
||||
|
@ -50,11 +54,10 @@ const ActiveTimelinesComponent: React.FC<ActiveTimelinesProps> = ({
|
|||
isOpen,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleToggleOpen = useCallback(
|
||||
() => dispatch(timelineActions.showTimeline({ id: timelineId, show: !isOpen })),
|
||||
[dispatch, isOpen, timelineId]
|
||||
);
|
||||
const handleToggleOpen = useCallback(() => {
|
||||
dispatch(timelineActions.showTimeline({ id: timelineId, show: !isOpen }));
|
||||
focusActiveTimelineButton();
|
||||
}, [dispatch, isOpen, timelineId]);
|
||||
|
||||
const title = !isEmpty(timelineTitle)
|
||||
? timelineTitle
|
||||
|
@ -83,6 +86,7 @@ const ActiveTimelinesComponent: React.FC<ActiveTimelinesProps> = ({
|
|||
<ButtonWrapper grow={false}>
|
||||
<StyledEuiButtonEmpty
|
||||
aria-label={i18n.TIMELINE_TOGGLE_BUTTON_ARIA_LABEL({ isOpen, title })}
|
||||
className={ACTIVE_TIMELINE_BUTTON_CLASS_NAME}
|
||||
flush="both"
|
||||
data-test-subj="flyoutOverlay"
|
||||
size="s"
|
||||
|
|
|
@ -33,6 +33,7 @@ import { ActiveTimelines } from './active_timelines';
|
|||
import * as i18n from './translations';
|
||||
import * as commonI18n from '../../timeline/properties/translations';
|
||||
import { getTimelineStatusByIdSelector } from './selectors';
|
||||
import { focusActiveTimelineButton } from '../../timeline/helpers';
|
||||
|
||||
// to hide side borders
|
||||
const StyledPanel = styled(EuiPanel)`
|
||||
|
@ -79,10 +80,10 @@ const FlyoutHeaderPanelComponent: React.FC<FlyoutHeaderPanelProps> = ({ timeline
|
|||
[dataProviders, kqlQuery]
|
||||
);
|
||||
|
||||
const handleClose = useCallback(
|
||||
() => dispatch(timelineActions.showTimeline({ id: timelineId, show: false })),
|
||||
[dispatch, timelineId]
|
||||
);
|
||||
const handleClose = useCallback(() => {
|
||||
dispatch(timelineActions.showTimeline({ id: timelineId, show: false }));
|
||||
focusActiveTimelineButton();
|
||||
}, [dispatch, timelineId]);
|
||||
|
||||
return (
|
||||
<StyledPanel
|
||||
|
|
|
@ -12,6 +12,7 @@ import { useDispatch } from 'react-redux';
|
|||
import { StatefulTimeline } from '../../timeline';
|
||||
import * as i18n from './translations';
|
||||
import { timelineActions } from '../../../store/timeline';
|
||||
import { focusActiveTimelineButton } from '../../timeline/helpers';
|
||||
|
||||
interface FlyoutPaneComponentProps {
|
||||
timelineId: string;
|
||||
|
@ -28,10 +29,10 @@ const EuiFlyoutContainer = styled.div`
|
|||
|
||||
const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({ timelineId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const handleClose = useCallback(
|
||||
() => dispatch(timelineActions.showTimeline({ id: timelineId, show: false })),
|
||||
[dispatch, timelineId]
|
||||
);
|
||||
const handleClose = useCallback(() => {
|
||||
dispatch(timelineActions.showTimeline({ id: timelineId, show: false }));
|
||||
focusActiveTimelineButton();
|
||||
}, [dispatch, timelineId]);
|
||||
|
||||
return (
|
||||
<EuiFlyoutContainer data-test-subj="flyout-pane">
|
||||
|
|
|
@ -17,18 +17,17 @@ import {
|
|||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
} from '@elastic/eui';
|
||||
import React, { useState, useCallback, useMemo, useRef } from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { State } from '../../../common/store';
|
||||
import { RowRendererId } from '../../../../common/types/timeline';
|
||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../store/timeline/actions';
|
||||
import { timelineSelectors } from '../../store/timeline';
|
||||
import { timelineDefaults } from '../../store/timeline/defaults';
|
||||
import { renderers } from './catalog';
|
||||
import { RowRenderersBrowser } from './row_renderers_browser';
|
||||
import * as i18n from './translations';
|
||||
|
||||
|
@ -81,7 +80,6 @@ interface StatefulRowRenderersBrowserProps {
|
|||
const StatefulRowRenderersBrowserComponent: React.FC<StatefulRowRenderersBrowserProps> = ({
|
||||
timelineId,
|
||||
}) => {
|
||||
const tableRef = useRef<EuiInMemoryTable<{}>>();
|
||||
const dispatch = useDispatch();
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const excludedRowRendererIds = useDeepEqualSelector(
|
||||
|
@ -105,12 +103,12 @@ const StatefulRowRenderersBrowserComponent: React.FC<StatefulRowRenderersBrowser
|
|||
const hideFieldBrowser = useCallback(() => setShow(false), []);
|
||||
|
||||
const handleDisableAll = useCallback(() => {
|
||||
tableRef?.current?.setSelection([]);
|
||||
}, [tableRef]);
|
||||
setExcludedRowRendererIds(Object.values(RowRendererId));
|
||||
}, [setExcludedRowRendererIds]);
|
||||
|
||||
const handleEnableAll = useCallback(() => {
|
||||
tableRef?.current?.setSelection(renderers);
|
||||
}, [tableRef]);
|
||||
setExcludedRowRendererIds([]);
|
||||
}, [setExcludedRowRendererIds]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -168,7 +166,6 @@ const StatefulRowRenderersBrowserComponent: React.FC<StatefulRowRenderersBrowser
|
|||
|
||||
<StyledEuiModalBody>
|
||||
<RowRenderersBrowser
|
||||
ref={tableRef}
|
||||
excludedRowRendererIds={excludedRowRendererIds}
|
||||
setExcludedRowRendererIds={setExcludedRowRendererIds}
|
||||
/>
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
* 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 { xor, xorBy } from 'lodash/fp';
|
||||
import { xor } from 'lodash/fp';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { RowRendererId } from '../../../../common/types/timeline';
|
||||
|
@ -76,21 +76,17 @@ const StyledNameButton = styled.button`
|
|||
text-align: left;
|
||||
`;
|
||||
|
||||
const RowRenderersBrowserComponent = React.forwardRef(
|
||||
({ excludedRowRendererIds = [], setExcludedRowRendererIds }: RowRenderersBrowserProps, ref) => {
|
||||
const notExcludedRowRenderers = useMemo(() => {
|
||||
if (excludedRowRendererIds.length === Object.keys(RowRendererId).length) return [];
|
||||
|
||||
return renderers.filter((renderer) => !excludedRowRendererIds.includes(renderer.id));
|
||||
}, [excludedRowRendererIds]);
|
||||
|
||||
const RowRenderersBrowserComponent = ({
|
||||
excludedRowRendererIds = [],
|
||||
setExcludedRowRendererIds,
|
||||
}: RowRenderersBrowserProps) => {
|
||||
const handleNameClick = useCallback(
|
||||
(item: RowRendererOption) => () => {
|
||||
const newSelection = xor([item], notExcludedRowRenderers);
|
||||
// @ts-expect-error
|
||||
ref?.current?.setSelection(newSelection);
|
||||
const newSelection = xor([item.id], excludedRowRendererIds);
|
||||
|
||||
setExcludedRowRendererIds(newSelection);
|
||||
},
|
||||
[notExcludedRowRenderers, ref]
|
||||
[excludedRowRendererIds, setExcludedRowRendererIds]
|
||||
);
|
||||
|
||||
const nameColumnRenderCallback = useCallback(
|
||||
|
@ -102,8 +98,26 @@ const RowRenderersBrowserComponent = React.forwardRef(
|
|||
[handleNameClick]
|
||||
);
|
||||
|
||||
const idColumnRenderCallback = useCallback(
|
||||
(_, item) => (
|
||||
<EuiCheckbox
|
||||
id={item.id}
|
||||
onChange={handleNameClick(item)}
|
||||
checked={!excludedRowRendererIds.includes(item.id)}
|
||||
/>
|
||||
),
|
||||
[excludedRowRendererIds, handleNameClick]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'id',
|
||||
name: '',
|
||||
sortable: false,
|
||||
width: '32px',
|
||||
render: idColumnRenderCallback,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
name: 'Name',
|
||||
|
@ -131,46 +145,20 @@ const RowRenderersBrowserComponent = React.forwardRef(
|
|||
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]
|
||||
[idColumnRenderCallback, nameColumnRenderCallback]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledEuiInMemoryTable
|
||||
ref={ref}
|
||||
items={renderers}
|
||||
itemId="id"
|
||||
columns={columns}
|
||||
search={search}
|
||||
sorting={initialSorting}
|
||||
isSelectable={true}
|
||||
selection={selectionValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
RowRenderersBrowserComponent.displayName = 'RowRenderersBrowserComponent';
|
||||
|
||||
|
|
|
@ -474,6 +474,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
|
|||
},
|
||||
]
|
||||
}
|
||||
tabType="query"
|
||||
timelineId="test"
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -4,19 +4,23 @@
|
|||
* 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 { Draggable } from 'react-beautiful-dnd';
|
||||
import { Resizable, ResizeCallback } from 're-resizable';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useDraggableKeyboardWrapper } from '../../../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook';
|
||||
import {
|
||||
DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME,
|
||||
getDraggableFieldId,
|
||||
} from '../../../../../common/components/drag_and_drop/helpers';
|
||||
import { TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
|
||||
import { OnFilterChange } from '../../events';
|
||||
import { Direction } from '../../../../../graphql/types';
|
||||
import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers';
|
||||
import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles';
|
||||
import { Sort } from '../sort';
|
||||
|
@ -24,6 +28,25 @@ import { Sort } from '../sort';
|
|||
import { Header } from './header';
|
||||
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 };
|
||||
|
||||
interface ColumneHeaderProps {
|
||||
|
@ -32,6 +55,7 @@ interface ColumneHeaderProps {
|
|||
isDragging: boolean;
|
||||
onFilterChange?: OnFilterChange;
|
||||
sort: Sort[];
|
||||
tabType: TimelineTabs;
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
|
@ -42,10 +66,11 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
|
|||
isDragging,
|
||||
onFilterChange,
|
||||
sort,
|
||||
tabType,
|
||||
}) => {
|
||||
const keyboardHandlerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [, setClosePopOverTrigger] = useState(false);
|
||||
const [, setHoverActionsOwnFocus] = useState<boolean>(false);
|
||||
const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState<boolean>(false);
|
||||
const restoreFocus = useCallback(() => keyboardHandlerRef.current?.focus(), []);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const resizableSize = useMemo(
|
||||
|
@ -84,10 +109,93 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
|
|||
const draggableId = useMemo(
|
||||
() =>
|
||||
getDraggableFieldId({
|
||||
contextId: `timeline-column-headers-${timelineId}`,
|
||||
contextId: `timeline-column-headers-${tabType}-${timelineId}`,
|
||||
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(
|
||||
|
@ -99,26 +207,28 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
|
|||
ref={dragProvided.innerRef}
|
||||
>
|
||||
<EventsThContent>
|
||||
<Header
|
||||
timelineId={timelineId}
|
||||
header={header}
|
||||
onFilterChange={onFilterChange}
|
||||
sort={sort}
|
||||
/>
|
||||
<PopoverContainer $width={header.width}>
|
||||
<EuiPopover
|
||||
anchorPosition="downLeft"
|
||||
button={headerButton}
|
||||
closePopover={handleClosePopOverTrigger}
|
||||
isOpen={hoverActionsOwnFocus}
|
||||
ownFocus
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<ContextMenu initialPanelId={0} panels={panels} />
|
||||
</EuiPopover>
|
||||
</PopoverContainer>
|
||||
</EventsThContent>
|
||||
</EventsTh>
|
||||
),
|
||||
[header, onFilterChange, sort, timelineId]
|
||||
[handleClosePopOverTrigger, headerButton, header.width, hoverActionsOwnFocus, panels]
|
||||
);
|
||||
|
||||
const onFocus = useCallback(() => {
|
||||
keyboardHandlerRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const handleClosePopOverTrigger = useCallback(() => {
|
||||
setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger);
|
||||
}, []);
|
||||
|
||||
const openPopover = useCallback(() => {
|
||||
setHoverActionsOwnFocus(true);
|
||||
}, []);
|
||||
|
@ -131,6 +241,15 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
|
|||
openPopover,
|
||||
});
|
||||
|
||||
const keyDownHandler = useCallback(
|
||||
(keyboardEvent: React.KeyboardEvent) => {
|
||||
if (!hoverActionsOwnFocus) {
|
||||
onKeyDown(keyboardEvent);
|
||||
}
|
||||
},
|
||||
[hoverActionsOwnFocus, onKeyDown]
|
||||
);
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
enable={RESIZABLE_ENABLE}
|
||||
|
@ -147,7 +266,7 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
|
|||
data-test-subj="draggableWrapperKeyboardHandler"
|
||||
onClick={onFocus}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDown={keyDownHandler}
|
||||
ref={keyboardHandlerRef}
|
||||
role="columnheader"
|
||||
tabIndex={0}
|
||||
|
@ -171,6 +290,7 @@ export const ColumnHeader = React.memo(
|
|||
ColumnHeaderComponent,
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.draggableIndex === nextProps.draggableIndex &&
|
||||
prevProps.tabType === nextProps.tabType &&
|
||||
prevProps.timelineId === nextProps.timelineId &&
|
||||
prevProps.isDragging === nextProps.isDragging &&
|
||||
prevProps.onFilterChange === nextProps.onFilterChange &&
|
||||
|
|
|
@ -19,6 +19,7 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended
|
|||
import { ColumnHeadersComponent } from '.';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { timelineActions } from '../../../../store/timeline';
|
||||
import { TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
|
@ -55,6 +56,7 @@ describe('ColumnHeaders', () => {
|
|||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={sort}
|
||||
tabType={TimelineTabs.query}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
|
@ -74,6 +76,7 @@ describe('ColumnHeaders', () => {
|
|||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={sort}
|
||||
tabType={TimelineTabs.query}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
|
@ -94,6 +97,7 @@ describe('ColumnHeaders', () => {
|
|||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={sort}
|
||||
tabType={TimelineTabs.query}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
|
@ -152,6 +156,7 @@ describe('ColumnHeaders', () => {
|
|||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={mockSort}
|
||||
tabType={TimelineTabs.query}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
|
@ -193,6 +198,7 @@ describe('ColumnHeaders', () => {
|
|||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={mockSort}
|
||||
tabType={TimelineTabs.query}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
|
@ -229,6 +235,7 @@ describe('ColumnHeaders', () => {
|
|||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={mockSort}
|
||||
tabType={TimelineTabs.query}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
|
|
|
@ -31,7 +31,7 @@ import {
|
|||
useGlobalFullScreen,
|
||||
useTimelineFullScreen,
|
||||
} from '../../../../../common/containers/use_full_screen';
|
||||
import { TimelineId } from '../../../../../../common/types/timeline';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { OnSelectAll } from '../../events';
|
||||
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
|
||||
import { StatefulFieldsBrowser } from '../../../fields_browser';
|
||||
|
@ -76,6 +76,7 @@ interface Props {
|
|||
showEventsSelect: boolean;
|
||||
showSelectAllCheckbox: boolean;
|
||||
sort: Sort[];
|
||||
tabType: TimelineTabs;
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
|
@ -122,6 +123,7 @@ export const ColumnHeadersComponent = ({
|
|||
showEventsSelect,
|
||||
showSelectAllCheckbox,
|
||||
sort,
|
||||
tabType,
|
||||
timelineId,
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -186,9 +188,10 @@ export const ColumnHeadersComponent = ({
|
|||
header={header}
|
||||
isDragging={draggingIndex === draggableIndex}
|
||||
sort={sort}
|
||||
tabType={tabType}
|
||||
/>
|
||||
)),
|
||||
[columnHeaders, timelineId, draggingIndex, sort]
|
||||
[columnHeaders, timelineId, draggingIndex, sort, tabType]
|
||||
);
|
||||
|
||||
const fullScreen = useMemo(
|
||||
|
@ -335,7 +338,7 @@ export const ColumnHeadersComponent = ({
|
|||
|
||||
<Droppable
|
||||
direction={'horizontal'}
|
||||
droppableId={`${droppableTimelineColumnsPrefix}${timelineId}`}
|
||||
droppableId={`${droppableTimelineColumnsPrefix}-${tabType}.${timelineId}`}
|
||||
isDropDisabled={false}
|
||||
type={DRAG_TYPE_FIELD}
|
||||
renderClone={renderClone}
|
||||
|
|
|
@ -22,10 +22,22 @@ export const FULL_SCREEN = i18n.translate('xpack.securitySolution.timeline.fullS
|
|||
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', {
|
||||
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', {
|
||||
defaultMessage: 'Type',
|
||||
});
|
||||
|
|
|
@ -195,6 +195,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={showCheckboxes}
|
||||
sort={sort}
|
||||
tabType={tabType}
|
||||
timelineId={id}
|
||||
/>
|
||||
|
||||
|
|
|
@ -6,9 +6,11 @@ exports[`Provider rendering renders correctly against snapshot 1`] = `
|
|||
field="name"
|
||||
isEnabled={true}
|
||||
isExcluded={false}
|
||||
isPopoverOpen={false}
|
||||
kqlQuery=""
|
||||
operator=":"
|
||||
providerId="id-Provider 1"
|
||||
setIsPopoverOpen={[Function]}
|
||||
toggleEnabledProvider={[Function]}
|
||||
toggleExcludedProvider={[Function]}
|
||||
toggleTypeProvider={[Function]}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { noop } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { DataProvider, DataProviderType, IS_OPERATOR } from './data_provider';
|
||||
import { ProviderItemBadge } from './provider_item_badge';
|
||||
|
@ -14,7 +14,10 @@ interface OwnProps {
|
|||
dataProvider: DataProvider;
|
||||
}
|
||||
|
||||
export const Provider = React.memo<OwnProps>(({ dataProvider }) => (
|
||||
export const Provider = React.memo<OwnProps>(({ dataProvider }) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<ProviderItemBadge
|
||||
deleteProvider={noop}
|
||||
field={dataProvider.queryMatch.displayField || dataProvider.queryMatch.field}
|
||||
|
@ -22,6 +25,8 @@ export const Provider = React.memo<OwnProps>(({ dataProvider }) => (
|
|||
isEnabled={dataProvider.enabled}
|
||||
isExcluded={dataProvider.excluded}
|
||||
providerId={dataProvider.id}
|
||||
isPopoverOpen={isPopoverOpen}
|
||||
setIsPopoverOpen={setIsPopoverOpen}
|
||||
toggleExcludedProvider={noop}
|
||||
toggleEnabledProvider={noop}
|
||||
toggleTypeProvider={noop}
|
||||
|
@ -29,6 +34,7 @@ export const Provider = React.memo<OwnProps>(({ dataProvider }) => (
|
|||
operator={dataProvider.queryMatch.operator || IS_OPERATOR}
|
||||
type={dataProvider.type || DataProviderType.default}
|
||||
/>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
Provider.displayName = 'Provider';
|
||||
|
|
|
@ -231,6 +231,7 @@ export class ProviderItemActions extends React.PureComponent<OwnProps> {
|
|||
button={button}
|
||||
anchorPosition="downCenter"
|
||||
panelPaddingSize="none"
|
||||
ownFocus={true}
|
||||
>
|
||||
<div style={{ userSelect: 'none' }}>
|
||||
<EuiContextMenu initialPanelId={0} panels={panelTree} data-test-subj="providerActions" />
|
||||
|
|
|
@ -28,10 +28,12 @@ interface ProviderItemBadgeProps {
|
|||
kqlQuery: string;
|
||||
isEnabled: boolean;
|
||||
isExcluded: boolean;
|
||||
isPopoverOpen: boolean;
|
||||
onDataProviderEdited?: OnDataProviderEdited;
|
||||
operator: QueryOperator;
|
||||
providerId: string;
|
||||
register?: DataProvidersAnd;
|
||||
setIsPopoverOpen: (isPopoverOpen: boolean) => void;
|
||||
timelineId?: string;
|
||||
toggleEnabledProvider: () => void;
|
||||
toggleExcludedProvider: () => void;
|
||||
|
@ -50,10 +52,12 @@ export const ProviderItemBadge = React.memo<ProviderItemBadgeProps>(
|
|||
kqlQuery,
|
||||
isEnabled,
|
||||
isExcluded,
|
||||
isPopoverOpen,
|
||||
onDataProviderEdited,
|
||||
operator,
|
||||
providerId,
|
||||
register,
|
||||
setIsPopoverOpen,
|
||||
timelineId,
|
||||
toggleEnabledProvider,
|
||||
toggleExcludedProvider,
|
||||
|
@ -75,16 +79,15 @@ export const ProviderItemBadge = React.memo<ProviderItemBadgeProps>(
|
|||
getManageTimelineById,
|
||||
timelineId,
|
||||
]);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const togglePopover = useCallback(() => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
}, [isPopoverOpen]);
|
||||
}, [isPopoverOpen, setIsPopoverOpen]);
|
||||
|
||||
const closePopover = useCallback(() => {
|
||||
setIsPopoverOpen(false);
|
||||
wrapperRef?.current?.focus();
|
||||
}, [wrapperRef]);
|
||||
}, [wrapperRef, setIsPopoverOpen]);
|
||||
|
||||
const onToggleEnabledProvider = useCallback(() => {
|
||||
toggleEnabledProvider();
|
||||
|
|
|
@ -155,7 +155,7 @@ interface DataProvidersGroupItem extends Omit<Props, 'dataProviders'> {
|
|||
export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
|
||||
({ browserFields, group, groupIndex, dataProvider, index, timelineId }) => {
|
||||
const keyboardHandlerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [, setHoverActionsOwnFocus] = useState<boolean>(false);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const [, setClosePopOverTrigger] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
@ -240,7 +240,7 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
|
|||
}, []);
|
||||
|
||||
const openPopover = useCallback(() => {
|
||||
setHoverActionsOwnFocus(true);
|
||||
setIsPopoverOpen(true);
|
||||
}, []);
|
||||
|
||||
const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
|
||||
|
@ -251,6 +251,15 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
|
|||
openPopover,
|
||||
});
|
||||
|
||||
const keyDownHandler = useCallback(
|
||||
(keyboardEvent: React.KeyboardEvent<Element>) => {
|
||||
if (keyboardHandlerRef.current === document.activeElement) {
|
||||
onKeyDown(keyboardEvent);
|
||||
}
|
||||
},
|
||||
[onKeyDown]
|
||||
);
|
||||
|
||||
const DraggableContent = useCallback(
|
||||
(provided, snapshot) => (
|
||||
<div
|
||||
|
@ -275,6 +284,7 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
|
|||
kqlQuery={index > 0 ? dataProvider.kqlQuery : group[0].kqlQuery}
|
||||
isEnabled={index > 0 ? dataProvider.enabled : group[0].enabled}
|
||||
isExcluded={index > 0 ? dataProvider.excluded : group[0].excluded}
|
||||
isPopoverOpen={isPopoverOpen}
|
||||
onDataProviderEdited={handleDataProviderEdited}
|
||||
operator={
|
||||
index > 0
|
||||
|
@ -284,6 +294,7 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
|
|||
register={dataProvider}
|
||||
providerId={index > 0 ? group[0].id : dataProvider.id}
|
||||
timelineId={timelineId}
|
||||
setIsPopoverOpen={setIsPopoverOpen}
|
||||
toggleEnabledProvider={handleToggleEnabledProvider}
|
||||
toggleExcludedProvider={handleToggleExcludedProvider}
|
||||
toggleTypeProvider={handleToggleTypeProvider}
|
||||
|
@ -315,7 +326,9 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
|
|||
handleToggleExcludedProvider,
|
||||
handleToggleTypeProvider,
|
||||
index,
|
||||
isPopoverOpen,
|
||||
keyboardHandlerRef,
|
||||
setIsPopoverOpen,
|
||||
timelineId,
|
||||
]
|
||||
);
|
||||
|
@ -326,7 +339,7 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>(
|
|||
data-test-subj="draggableWrapperKeyboardHandler"
|
||||
onClick={onFocus}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDown={keyDownHandler}
|
||||
ref={keyboardHandlerRef}
|
||||
role="button"
|
||||
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)`
|
||||
margin: 0 2px;
|
||||
vertical-align: middle;
|
||||
`;
|
||||
|
||||
|
|
Loading…
Reference in a new issue