mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] [Analyzer] Add independent date and index/dataview selector (#176364)
## Summary
Adds a date picker and index pattern selection component (the current
security solution wide version, sourcerer) to analyzer, independent from
the table that the analyzer was opened from. Date range will default to
1) the current range on the page, if 0 results are found in the tree api
request, 2) We retry the request with unbounded time, and set the date
picker to that value if that succeeds. If at any point in time when the
request is pending for either of those 2 requests, and the user uses the
new date picker to select a time, the previous request is cancelled and
a new one made with the user supplied time. Previous date picker values
are cleared on close. Index selection is updated when the user selects
the apply button in the sourcerer popover, and any modifications are
persisted across multiple different instances of analyzer.

### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
parent
5e56ad222a
commit
512967cc44
29 changed files with 1304 additions and 798 deletions
|
@ -192,6 +192,12 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
*/
|
||||
timelineEsqlTabDisabled: false,
|
||||
|
||||
/*
|
||||
* Disables date pickers and sourcerer in analyzer if needed.
|
||||
*
|
||||
*/
|
||||
analyzerDatePickersAndSourcererDisabled: false,
|
||||
|
||||
/**
|
||||
* Enables per-field rule diffs tab in the prebuilt rule upgrade flyout
|
||||
*
|
||||
|
|
|
@ -659,4 +659,13 @@ describe('Sourcerer component', () => {
|
|||
);
|
||||
expect(pollForSignalIndexMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders without a popover when analyzer is the scope', () => {
|
||||
mount(
|
||||
<TestProviders>
|
||||
<Sourcerer scope={sourcererModel.SourcererScopeName.analyzer} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find(`[data-test-subj="sourcerer-popover"]`).exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import {
|
||||
EuiComboBox,
|
||||
EuiForm,
|
||||
EuiOutsideClickDetector,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiSpacer,
|
||||
|
@ -23,6 +22,7 @@ import type { sourcererModel } from '../../store/sourcerer';
|
|||
import { sourcererActions, sourcererSelectors } from '../../store/sourcerer';
|
||||
import { useDeepEqualSelector } from '../../hooks/use_selector';
|
||||
import type { SourcererUrlState } from '../../store/sourcerer/model';
|
||||
import type { ModifiedTypes } from './use_pick_index_patterns';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { usePickIndexPatterns } from './use_pick_index_patterns';
|
||||
import { FormRow, PopoverContent, StyledButtonEmpty, StyledFormRow } from './helpers';
|
||||
|
@ -39,6 +39,89 @@ export interface SourcererComponentProps {
|
|||
scope: sourcererModel.SourcererScopeName;
|
||||
}
|
||||
|
||||
interface SourcererPopoverProps {
|
||||
showSourcerer: boolean;
|
||||
activePatterns?: string[];
|
||||
isTriggerDisabled: boolean;
|
||||
isModified: ModifiedTypes;
|
||||
isOnlyDetectionAlerts: boolean;
|
||||
isPopoverOpen: boolean;
|
||||
loading: boolean;
|
||||
setPopoverIsOpenCb: () => void;
|
||||
selectedPatterns: string[];
|
||||
signalIndexName: string | null;
|
||||
handleClosePopOver: () => void;
|
||||
isTimelineSourcerer: boolean;
|
||||
selectedDataViewId: string | null;
|
||||
sourcererMissingPatterns: string[];
|
||||
onUpdateDetectionAlertsChecked: () => void;
|
||||
handleOutsideClick: () => void;
|
||||
setMissingPatterns: (missingPatterns: string[]) => void;
|
||||
setDataViewId: (dataViewId: string | null) => void;
|
||||
scopeId: sourcererModel.SourcererScopeName;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const SourcererPopover = React.memo<SourcererPopoverProps>(
|
||||
({
|
||||
showSourcerer,
|
||||
activePatterns,
|
||||
isTriggerDisabled,
|
||||
isModified,
|
||||
isOnlyDetectionAlerts,
|
||||
isPopoverOpen,
|
||||
loading,
|
||||
setPopoverIsOpenCb,
|
||||
selectedPatterns,
|
||||
signalIndexName,
|
||||
handleClosePopOver,
|
||||
isTimelineSourcerer,
|
||||
selectedDataViewId,
|
||||
sourcererMissingPatterns,
|
||||
onUpdateDetectionAlertsChecked,
|
||||
setMissingPatterns,
|
||||
setDataViewId,
|
||||
scopeId,
|
||||
children,
|
||||
}) => {
|
||||
if (!showSourcerer) {
|
||||
return null;
|
||||
} else if (scopeId === SourcererScopeName.analyzer) {
|
||||
return <>{children}</>;
|
||||
} else {
|
||||
return (
|
||||
<EuiPopover
|
||||
panelClassName="sourcererPopoverPanel"
|
||||
button={
|
||||
<Trigger
|
||||
activePatterns={activePatterns}
|
||||
disabled={isTriggerDisabled}
|
||||
isModified={isModified}
|
||||
isOnlyDetectionAlerts={isOnlyDetectionAlerts}
|
||||
isPopoverOpen={isPopoverOpen}
|
||||
isTimelineSourcerer={isTimelineSourcerer}
|
||||
loading={loading}
|
||||
onClick={setPopoverIsOpenCb}
|
||||
selectedPatterns={selectedPatterns}
|
||||
signalIndexName={signalIndexName}
|
||||
/>
|
||||
}
|
||||
closePopover={handleClosePopOver}
|
||||
data-test-subj={isTimelineSourcerer ? 'timeline-sourcerer-popover' : 'sourcerer-popover'}
|
||||
display="block"
|
||||
isOpen={isPopoverOpen}
|
||||
ownFocus
|
||||
repositionOnScroll
|
||||
>
|
||||
<>{children}</>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
SourcererPopover.displayName = 'SourcererPopover';
|
||||
|
||||
export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isDetectionsSourcerer = scopeId === SourcererScopeName.detections;
|
||||
|
@ -57,7 +140,6 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
missingPatterns: sourcererMissingPatterns,
|
||||
},
|
||||
} = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId));
|
||||
|
||||
const { pollForSignalIndex } = useSignalHelpers();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -188,9 +270,18 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
}, [dispatchChangeDataView, dataViewId, selectedOptions]);
|
||||
|
||||
const handleClosePopOver = useCallback(() => {
|
||||
handleOutsideClick();
|
||||
setDataViewId(selectedDataViewId);
|
||||
setMissingPatterns(sourcererMissingPatterns);
|
||||
onUpdateDetectionAlertsChecked();
|
||||
setPopoverIsOpen(false);
|
||||
setExpandAdvancedOptions(false);
|
||||
}, []);
|
||||
}, [
|
||||
handleOutsideClick,
|
||||
onUpdateDetectionAlertsChecked,
|
||||
selectedDataViewId,
|
||||
sourcererMissingPatterns,
|
||||
]);
|
||||
|
||||
// deprecated timeline index pattern handlers
|
||||
const onContinueUpdateDeprecated = useCallback(() => {
|
||||
|
@ -247,137 +338,126 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
setDataViewId(selectedDataViewId);
|
||||
}, [selectedDataViewId]);
|
||||
|
||||
const onOutsideClick = useCallback(() => {
|
||||
setDataViewId(selectedDataViewId);
|
||||
setMissingPatterns(sourcererMissingPatterns);
|
||||
onUpdateDetectionAlertsChecked();
|
||||
handleOutsideClick();
|
||||
}, [
|
||||
handleOutsideClick,
|
||||
onUpdateDetectionAlertsChecked,
|
||||
selectedDataViewId,
|
||||
sourcererMissingPatterns,
|
||||
]);
|
||||
|
||||
const onExpandAdvancedOptionsClicked = useCallback(() => {
|
||||
setExpandAdvancedOptions((prevState) => !prevState);
|
||||
}, []);
|
||||
|
||||
// always show sourcerer in timeline
|
||||
return indicesExist || scopeId === SourcererScopeName.timeline ? (
|
||||
<EuiPopover
|
||||
panelClassName="sourcererPopoverPanel"
|
||||
button={
|
||||
<Trigger
|
||||
activePatterns={activePatterns}
|
||||
disabled={isTriggerDisabled}
|
||||
isModified={isModified}
|
||||
isOnlyDetectionAlerts={isOnlyDetectionAlerts}
|
||||
isPopoverOpen={isPopoverOpen}
|
||||
isTimelineSourcerer={isTimelineSourcerer}
|
||||
loading={loading}
|
||||
onClick={setPopoverIsOpenCb}
|
||||
selectedPatterns={selectedPatterns}
|
||||
signalIndexName={signalIndexName}
|
||||
/>
|
||||
}
|
||||
closePopover={handleClosePopOver}
|
||||
data-test-subj={isTimelineSourcerer ? 'timeline-sourcerer-popover' : 'sourcerer-popover'}
|
||||
display="block"
|
||||
isOpen={isPopoverOpen}
|
||||
ownFocus
|
||||
repositionOnScroll
|
||||
const showSourcerer = useMemo(() => {
|
||||
return (
|
||||
indicesExist || [SourcererScopeName.analyzer, SourcererScopeName.timeline].includes(scopeId)
|
||||
);
|
||||
}, [indicesExist, scopeId]);
|
||||
|
||||
return (
|
||||
<SourcererPopover
|
||||
showSourcerer={showSourcerer}
|
||||
activePatterns={activePatterns}
|
||||
isTriggerDisabled={isTriggerDisabled}
|
||||
isModified={isModified}
|
||||
isOnlyDetectionAlerts={isOnlyDetectionAlerts}
|
||||
isPopoverOpen={isPopoverOpen}
|
||||
isTimelineSourcerer={isTimelineSourcerer}
|
||||
loading={loading}
|
||||
handleOutsideClick={handleOutsideClick}
|
||||
setPopoverIsOpenCb={setPopoverIsOpenCb}
|
||||
selectedPatterns={selectedPatterns}
|
||||
signalIndexName={signalIndexName}
|
||||
handleClosePopOver={handleClosePopOver}
|
||||
selectedDataViewId={selectedDataViewId}
|
||||
sourcererMissingPatterns={sourcererMissingPatterns}
|
||||
onUpdateDetectionAlertsChecked={onUpdateDetectionAlertsChecked}
|
||||
setMissingPatterns={setMissingPatterns}
|
||||
setDataViewId={setDataViewId}
|
||||
scopeId={scopeId}
|
||||
>
|
||||
<EuiOutsideClickDetector onOutsideClick={onOutsideClick}>
|
||||
<PopoverContent>
|
||||
<EuiPopoverTitle data-test-subj="sourcerer-title">
|
||||
<>{i18n.SELECT_DATA_VIEW}</>
|
||||
</EuiPopoverTitle>
|
||||
<SourcererCallout
|
||||
isOnlyDetectionAlerts={isOnlyDetectionAlerts}
|
||||
title={isTimelineSourcerer ? i18n.CALL_OUT_TIMELINE_TITLE : i18n.CALL_OUT_TITLE}
|
||||
<PopoverContent>
|
||||
<EuiPopoverTitle data-test-subj="sourcerer-title">
|
||||
<>{i18n.SELECT_DATA_VIEW}</>
|
||||
</EuiPopoverTitle>
|
||||
<SourcererCallout
|
||||
isOnlyDetectionAlerts={isOnlyDetectionAlerts}
|
||||
title={isTimelineSourcerer ? i18n.CALL_OUT_TIMELINE_TITLE : i18n.CALL_OUT_TITLE}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
{(dataViewId === null && isModified === 'deprecated') ||
|
||||
isModified === 'missingPatterns' ? (
|
||||
<TemporarySourcerer
|
||||
activePatterns={activePatterns}
|
||||
indicesExist={indicesExist}
|
||||
isModified={isModified}
|
||||
isShowingUpdateModal={isShowingUpdateModal}
|
||||
missingPatterns={missingPatterns}
|
||||
onContinueWithoutUpdate={onContinueUpdateDeprecated}
|
||||
onDismiss={setPopoverIsOpenCb}
|
||||
onDismissModal={() => setIsShowingUpdateModal(false)}
|
||||
onReset={resetDataSources}
|
||||
onUpdateStepOne={isModified === 'deprecated' ? onUpdateDeprecated : onUpdateDataView}
|
||||
onUpdateStepTwo={onUpdateDataView}
|
||||
selectedPatterns={selectedPatterns}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
{(dataViewId === null && isModified === 'deprecated') ||
|
||||
isModified === 'missingPatterns' ? (
|
||||
<TemporarySourcerer
|
||||
activePatterns={activePatterns}
|
||||
indicesExist={indicesExist}
|
||||
isModified={isModified}
|
||||
isShowingUpdateModal={isShowingUpdateModal}
|
||||
missingPatterns={missingPatterns}
|
||||
onContinueWithoutUpdate={onContinueUpdateDeprecated}
|
||||
onDismiss={setPopoverIsOpenCb}
|
||||
onDismissModal={() => setIsShowingUpdateModal(false)}
|
||||
onReset={resetDataSources}
|
||||
onUpdateStepOne={isModified === 'deprecated' ? onUpdateDeprecated : onUpdateDataView}
|
||||
onUpdateStepTwo={onUpdateDataView}
|
||||
selectedPatterns={selectedPatterns}
|
||||
/>
|
||||
) : (
|
||||
<EuiForm component="form">
|
||||
<>
|
||||
<AlertsCheckbox
|
||||
isShow={isTimelineSourcerer}
|
||||
checked={isOnlyDetectionAlertsChecked}
|
||||
onChange={onCheckboxChanged}
|
||||
/>
|
||||
{dataViewId && (
|
||||
<StyledFormRow label={i18n.INDEX_PATTERNS_CHOOSE_DATA_VIEW_LABEL}>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="sourcerer-select"
|
||||
isLoading={loadingIndexPatterns}
|
||||
disabled={isOnlyDetectionAlerts}
|
||||
fullWidth
|
||||
onChange={onChangeDataView}
|
||||
options={dataViewSelectOptions}
|
||||
placeholder={i18n.INDEX_PATTERNS_CHOOSE_DATA_VIEW_LABEL}
|
||||
valueOfSelected={dataViewId}
|
||||
/>
|
||||
</StyledFormRow>
|
||||
)}
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<StyledButtonEmpty
|
||||
color="text"
|
||||
data-test-subj="sourcerer-advanced-options-toggle"
|
||||
iconType={expandAdvancedOptions ? 'arrowDown' : 'arrowRight'}
|
||||
onClick={onExpandAdvancedOptionsClicked}
|
||||
>
|
||||
{i18n.INDEX_PATTERNS_ADVANCED_OPTIONS_TITLE}
|
||||
</StyledButtonEmpty>
|
||||
{expandAdvancedOptions && <EuiSpacer size="m" />}
|
||||
<FormRow
|
||||
isDisabled={loadingIndexPatterns}
|
||||
$expandAdvancedOptions={expandAdvancedOptions}
|
||||
helpText={isOnlyDetectionAlerts ? undefined : i18n.INDEX_PATTERNS_DESCRIPTIONS}
|
||||
label={i18n.INDEX_PATTERNS_LABEL}
|
||||
>
|
||||
<EuiComboBox
|
||||
data-test-subj="sourcerer-combo-box"
|
||||
) : (
|
||||
<EuiForm component="form">
|
||||
<>
|
||||
<AlertsCheckbox
|
||||
isShow={isTimelineSourcerer}
|
||||
checked={isOnlyDetectionAlertsChecked}
|
||||
onChange={onCheckboxChanged}
|
||||
/>
|
||||
{dataViewId && (
|
||||
<StyledFormRow label={i18n.INDEX_PATTERNS_CHOOSE_DATA_VIEW_LABEL}>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="sourcerer-select"
|
||||
isLoading={loadingIndexPatterns}
|
||||
disabled={isOnlyDetectionAlerts}
|
||||
fullWidth
|
||||
isDisabled={isOnlyDetectionAlerts || loadingIndexPatterns}
|
||||
onChange={onChangeIndexPatterns}
|
||||
options={allOptions}
|
||||
placeholder={i18n.PICK_INDEX_PATTERNS}
|
||||
renderOption={renderOption}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={onChangeDataView}
|
||||
options={dataViewSelectOptions}
|
||||
placeholder={i18n.INDEX_PATTERNS_CHOOSE_DATA_VIEW_LABEL}
|
||||
valueOfSelected={dataViewId}
|
||||
/>
|
||||
</FormRow>
|
||||
</StyledFormRow>
|
||||
)}
|
||||
|
||||
<SaveButtons
|
||||
disableSave={selectedOptions.length === 0}
|
||||
isShow={!isDetectionsSourcerer}
|
||||
onReset={resetDataSources}
|
||||
onSave={handleSaveIndices}
|
||||
<EuiSpacer size="m" />
|
||||
<StyledButtonEmpty
|
||||
color="text"
|
||||
data-test-subj="sourcerer-advanced-options-toggle"
|
||||
iconType={expandAdvancedOptions ? 'arrowDown' : 'arrowRight'}
|
||||
onClick={onExpandAdvancedOptionsClicked}
|
||||
>
|
||||
{i18n.INDEX_PATTERNS_ADVANCED_OPTIONS_TITLE}
|
||||
</StyledButtonEmpty>
|
||||
{expandAdvancedOptions && <EuiSpacer size="m" />}
|
||||
<FormRow
|
||||
isDisabled={loadingIndexPatterns}
|
||||
$expandAdvancedOptions={expandAdvancedOptions}
|
||||
helpText={isOnlyDetectionAlerts ? undefined : i18n.INDEX_PATTERNS_DESCRIPTIONS}
|
||||
label={i18n.INDEX_PATTERNS_LABEL}
|
||||
>
|
||||
<EuiComboBox
|
||||
data-test-subj="sourcerer-combo-box"
|
||||
fullWidth
|
||||
isDisabled={isOnlyDetectionAlerts || loadingIndexPatterns}
|
||||
onChange={onChangeIndexPatterns}
|
||||
options={allOptions}
|
||||
placeholder={i18n.PICK_INDEX_PATTERNS}
|
||||
renderOption={renderOption}
|
||||
selectedOptions={selectedOptions}
|
||||
/>
|
||||
</>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiForm>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</EuiOutsideClickDetector>
|
||||
</EuiPopover>
|
||||
) : null;
|
||||
</FormRow>
|
||||
|
||||
<SaveButtons
|
||||
disableSave={selectedOptions.length === 0}
|
||||
isShow={!isDetectionsSourcerer}
|
||||
onReset={resetDataSources}
|
||||
onSave={handleSaveIndices}
|
||||
/>
|
||||
</>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiForm>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</SourcererPopover>
|
||||
);
|
||||
});
|
||||
Sourcerer.displayName = 'Sourcerer';
|
||||
|
|
|
@ -67,12 +67,16 @@ export const usePickIndexPatterns = ({
|
|||
} = useKibana().services;
|
||||
const isHookAlive = useRef(true);
|
||||
const [loadingIndexPatterns, setLoadingIndexPatterns] = useState(false);
|
||||
const alertsOptions = useMemo(
|
||||
() => (signalIndexName ? patternListToOptions([signalIndexName]) : []),
|
||||
[signalIndexName]
|
||||
);
|
||||
// anything that uses patternListToOptions should be memoized, as it always returns a new array
|
||||
// TODO: fix that
|
||||
const signalPatternListToOptions = useMemo(() => {
|
||||
return signalIndexName ? patternListToOptions([signalIndexName]) : [];
|
||||
}, [signalIndexName]);
|
||||
const selectedPatternsAsOptions = useMemo(() => {
|
||||
return patternListToOptions(selectedPatterns);
|
||||
}, [selectedPatterns]);
|
||||
const [selectedOptions, setSelectedOptions] = useState<Array<EuiComboBoxOptionOption<string>>>(
|
||||
isOnlyDetectionAlerts ? alertsOptions : patternListToOptions(selectedPatterns)
|
||||
isOnlyDetectionAlerts ? signalPatternListToOptions : selectedPatternsAsOptions
|
||||
);
|
||||
const [isModified, setIsModified] = useState<ModifiedTypes>(
|
||||
dataViewId == null ? 'deprecated' : missingPatterns.length > 0 ? 'missingPatterns' : ''
|
||||
|
@ -121,7 +125,7 @@ export const usePickIndexPatterns = ({
|
|||
const getDefaultSelectedOptionsByDataView = useCallback(
|
||||
(id: string, isAlerts: boolean = false): Array<EuiComboBoxOptionOption<string>> =>
|
||||
scopeId === SourcererScopeName.detections || isAlerts
|
||||
? alertsOptions
|
||||
? signalPatternListToOptions
|
||||
: patternListToOptions(
|
||||
getScopePatternListSelection(
|
||||
kibanaDataViews.find((dataView) => dataView.id === id),
|
||||
|
@ -130,7 +134,7 @@ export const usePickIndexPatterns = ({
|
|||
id === defaultDataViewId
|
||||
)
|
||||
),
|
||||
[alertsOptions, kibanaDataViews, scopeId, signalIndexName, defaultDataViewId]
|
||||
[signalPatternListToOptions, kibanaDataViews, scopeId, signalIndexName, defaultDataViewId]
|
||||
);
|
||||
|
||||
const defaultSelectedPatternsAsOptions = useMemo(
|
||||
|
@ -162,11 +166,10 @@ export const usePickIndexPatterns = ({
|
|||
useEffect(() => {
|
||||
setSelectedOptions(
|
||||
scopeId === SourcererScopeName.detections
|
||||
? alertsOptions
|
||||
: patternListToOptions(selectedPatterns)
|
||||
? signalPatternListToOptions
|
||||
: selectedPatternsAsOptions
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedPatterns, scopeId]);
|
||||
}, [selectedPatterns, scopeId, selectedPatternsAsOptions, signalPatternListToOptions]);
|
||||
// when scope updates, check modified to set/remove alerts label
|
||||
useEffect(() => {
|
||||
onSetIsModified(
|
||||
|
@ -200,7 +203,9 @@ export const usePickIndexPatterns = ({
|
|||
if (isHookAlive.current) {
|
||||
dispatch(sourcererActions.setDataView(dataView));
|
||||
setSelectedOptions(
|
||||
isOnlyDetectionAlerts ? alertsOptions : patternListToOptions(dataView.patternList)
|
||||
isOnlyDetectionAlerts
|
||||
? signalPatternListToOptions
|
||||
: patternListToOptions(dataView.patternList)
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -212,7 +217,7 @@ export const usePickIndexPatterns = ({
|
|||
}
|
||||
},
|
||||
[
|
||||
alertsOptions,
|
||||
signalPatternListToOptions,
|
||||
dispatch,
|
||||
getDefaultSelectedOptionsByDataView,
|
||||
isOnlyDetectionAlerts,
|
||||
|
@ -243,8 +248,8 @@ export const usePickIndexPatterns = ({
|
|||
}, []);
|
||||
|
||||
const handleOutsideClick = useCallback(() => {
|
||||
setSelectedOptions(patternListToOptions(selectedPatterns));
|
||||
}, [selectedPatterns]);
|
||||
setSelectedOptions(selectedPatternsAsOptions);
|
||||
}, [selectedPatternsAsOptions]);
|
||||
|
||||
return {
|
||||
allOptions,
|
||||
|
|
|
@ -161,7 +161,7 @@ describe('Sourcerer Hooks', () => {
|
|||
});
|
||||
await waitForNextUpdate();
|
||||
rerender();
|
||||
expect(mockDispatch).toBeCalledTimes(2);
|
||||
expect(mockDispatch).toBeCalledTimes(3);
|
||||
expect(mockDispatch.mock.calls[0][0]).toEqual({
|
||||
type: 'x-pack/security_solution/local/sourcerer/SET_DATA_VIEW_LOADING',
|
||||
payload: { id: 'security-solution', loading: true },
|
||||
|
@ -207,30 +207,30 @@ describe('Sourcerer Hooks', () => {
|
|||
await waitForNextUpdate();
|
||||
rerender();
|
||||
await waitFor(() => {
|
||||
expect(mockDispatch.mock.calls[2][0]).toEqual({
|
||||
expect(mockDispatch.mock.calls[3][0]).toEqual({
|
||||
type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING',
|
||||
payload: { loading: true },
|
||||
});
|
||||
expect(mockDispatch.mock.calls[3][0]).toEqual({
|
||||
expect(mockDispatch.mock.calls[4][0]).toEqual({
|
||||
type: 'x-pack/security_solution/local/sourcerer/SET_SIGNAL_INDEX_NAME',
|
||||
payload: { signalIndexName: mockSourcererState.signalIndexName },
|
||||
});
|
||||
expect(mockDispatch.mock.calls[4][0]).toEqual({
|
||||
expect(mockDispatch.mock.calls[5][0]).toEqual({
|
||||
type: 'x-pack/security_solution/local/sourcerer/SET_DATA_VIEW_LOADING',
|
||||
payload: {
|
||||
id: mockSourcererState.defaultDataView.id,
|
||||
loading: true,
|
||||
},
|
||||
});
|
||||
expect(mockDispatch.mock.calls[5][0]).toEqual({
|
||||
expect(mockDispatch.mock.calls[6][0]).toEqual({
|
||||
type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_DATA_VIEWS',
|
||||
payload: mockNewDataViews,
|
||||
});
|
||||
expect(mockDispatch.mock.calls[6][0]).toEqual({
|
||||
expect(mockDispatch.mock.calls[7][0]).toEqual({
|
||||
type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING',
|
||||
payload: { loading: false },
|
||||
});
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(7);
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(8);
|
||||
expect(mockSearch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
@ -396,7 +396,7 @@ describe('Sourcerer Hooks', () => {
|
|||
);
|
||||
await waitForNextUpdate();
|
||||
rerender();
|
||||
expect(mockDispatch.mock.calls[2][0]).toEqual({
|
||||
expect(mockDispatch.mock.calls[3][0]).toEqual({
|
||||
type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_DATA_VIEW',
|
||||
payload: {
|
||||
id: 'detections',
|
||||
|
|
|
@ -207,6 +207,18 @@ export const useInitSourcerer = (
|
|||
),
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
sourcererActions.setSelectedDataView({
|
||||
id: SourcererScopeName.analyzer,
|
||||
selectedDataViewId: defaultDataView.id,
|
||||
selectedPatterns: getScopePatternListSelection(
|
||||
defaultDataView,
|
||||
SourcererScopeName.analyzer,
|
||||
signalIndexName,
|
||||
true
|
||||
),
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
signalIndexNameSourcerer != null &&
|
||||
(activeTimeline == null || activeTimeline.savedObjectId == null) &&
|
||||
|
@ -226,6 +238,18 @@ export const useInitSourcerer = (
|
|||
),
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
sourcererActions.setSelectedDataView({
|
||||
id: SourcererScopeName.analyzer,
|
||||
selectedDataViewId: defaultDataView.id,
|
||||
selectedPatterns: getScopePatternListSelection(
|
||||
defaultDataView,
|
||||
SourcererScopeName.analyzer,
|
||||
signalIndexNameSourcerer,
|
||||
true
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [
|
||||
activeTimeline,
|
||||
|
@ -425,26 +449,26 @@ export const useSourcererDataView = (
|
|||
[legacyDataView, missingPatterns.length, selectedDataView]
|
||||
);
|
||||
|
||||
const indicesExist = useMemo(
|
||||
() =>
|
||||
loading || sourcererDataView.loading
|
||||
? true
|
||||
: checkIfIndicesExist({
|
||||
scopeId,
|
||||
signalIndexName,
|
||||
patternList: sourcererDataView.patternList,
|
||||
isDefaultDataViewSelected: sourcererDataView.id === defaultDataView.id,
|
||||
}),
|
||||
[
|
||||
defaultDataView.id,
|
||||
loading,
|
||||
scopeId,
|
||||
signalIndexName,
|
||||
sourcererDataView.id,
|
||||
sourcererDataView.loading,
|
||||
sourcererDataView.patternList,
|
||||
]
|
||||
);
|
||||
const indicesExist = useMemo(() => {
|
||||
if (loading || sourcererDataView.loading) {
|
||||
return true;
|
||||
} else {
|
||||
return checkIfIndicesExist({
|
||||
scopeId,
|
||||
signalIndexName,
|
||||
patternList: sourcererDataView.patternList,
|
||||
isDefaultDataViewSelected: sourcererDataView.id === defaultDataView.id,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
defaultDataView.id,
|
||||
loading,
|
||||
scopeId,
|
||||
signalIndexName,
|
||||
sourcererDataView.id,
|
||||
sourcererDataView.loading,
|
||||
sourcererDataView.patternList,
|
||||
]);
|
||||
|
||||
const browserFields = useCallback(() => {
|
||||
const { browserFields: dataViewBrowserFields } = getDataViewStateFromIndexFields(
|
||||
|
|
|
@ -481,6 +481,16 @@ export const mockGlobalState: State = {
|
|||
true
|
||||
),
|
||||
},
|
||||
[SourcererScopeName.analyzer]: {
|
||||
...mockSourcererState.sourcererScopes[SourcererScopeName.default],
|
||||
selectedDataViewId: mockSourcererState.defaultDataView.id,
|
||||
selectedPatterns: getScopePatternListSelection(
|
||||
mockSourcererState.defaultDataView,
|
||||
SourcererScopeName.default,
|
||||
mockSourcererState.signalIndexName,
|
||||
true
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
globalUrlParam: {},
|
||||
|
|
|
@ -26,6 +26,8 @@ const getPatternListFromScope = (
|
|||
return signalIndexName != null ? [signalIndexName] : [];
|
||||
case SourcererScopeName.timeline:
|
||||
return sortWithExcludesAtEnd(patternList);
|
||||
case SourcererScopeName.analyzer:
|
||||
return sortWithExcludesAtEnd(patternList);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ export enum SourcererScopeName {
|
|||
default = 'default',
|
||||
detections = 'detections',
|
||||
timeline = 'timeline',
|
||||
analyzer = 'analyzer',
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -189,5 +190,9 @@ export const initialSourcererState: SourcererModel = {
|
|||
...initSourcererScope,
|
||||
id: SourcererScopeName.timeline,
|
||||
},
|
||||
[SourcererScopeName.analyzer]: {
|
||||
...initSourcererScope,
|
||||
id: SourcererScopeName.analyzer,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -191,6 +191,16 @@ export const userReloadedResolverNode = actionCreator<{
|
|||
readonly nodeID: string;
|
||||
}>('USER_RELOADED_RESOLVER_NODE');
|
||||
|
||||
export const userOverrodeDateRange = actionCreator<{
|
||||
readonly id: string;
|
||||
readonly timeRange: TimeFilters;
|
||||
}>('USER_OVERRODE_DATE_RANGE');
|
||||
|
||||
export const userOverrodeSourcererSelection = actionCreator<{
|
||||
readonly id: string;
|
||||
readonly indices: string[];
|
||||
}>('USER_OVERRODE_SOURCERER_SELECTION');
|
||||
|
||||
/**
|
||||
* When the server returns an error after the app requests node data for a set of nodes.
|
||||
*/
|
||||
|
|
|
@ -16,10 +16,27 @@ import { endpointSourceSchema, winlogSourceSchema } from '../../mocks/tree_schem
|
|||
import type { NewResolverTree, ResolverSchema } from '../../../../common/endpoint/types';
|
||||
import { ancestorsWithAncestryField, descendantsLimit } from '../../models/resolver_tree';
|
||||
import { EMPTY_RESOLVER } from '../helpers';
|
||||
import { serverReturnedResolverData } from './action';
|
||||
import { serverReturnedResolverData, userOverrodeDateRange } from './action';
|
||||
import { appReceivedNewExternalProperties } from '../actions';
|
||||
|
||||
type SourceAndSchemaFunction = () => { schema: ResolverSchema; dataSource: string };
|
||||
|
||||
jest.mock('../../../common/utils/default_date_settings', () => {
|
||||
const original = jest.requireActual('../../../common/utils/default_date_settings');
|
||||
return {
|
||||
...original,
|
||||
getTimeRangeSettings: () => ({ to: '', from: '' }),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../common/utils/normalize_time_range', () => {
|
||||
const original = jest.requireActual('../../../common/utils/normalize_time_range');
|
||||
return {
|
||||
...original,
|
||||
normalizeTimeRange: () => original.normalizeTimeRange(false),
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Test the data reducer and selector.
|
||||
*/
|
||||
|
@ -187,6 +204,41 @@ describe('Resolver Data Middleware', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when a user sets a custom time range', () => {
|
||||
beforeEach(() => {
|
||||
const from = 'Sep 21, 2024 @ 20:49:13.452';
|
||||
const to = 'Sep 21, 2024 @ 20:49:13.452';
|
||||
dispatchTree(generatedTreeMetadata.formattedTree, winlogSourceSchema);
|
||||
store.dispatch(
|
||||
appReceivedNewExternalProperties({
|
||||
id,
|
||||
resolverComponentInstanceID: id,
|
||||
locationSearch: '',
|
||||
databaseDocumentID: id,
|
||||
filters: {},
|
||||
indices: ['index1'],
|
||||
shouldUpdate: false,
|
||||
})
|
||||
);
|
||||
store.dispatch(
|
||||
userOverrodeDateRange({
|
||||
id,
|
||||
timeRange: { from, to },
|
||||
})
|
||||
);
|
||||
});
|
||||
it('should use that time over anything else', () => {
|
||||
const params = selectors.treeParametersToFetch(store.getState()[id].data);
|
||||
if (params?.filters !== undefined) {
|
||||
const {
|
||||
filters: { from, to },
|
||||
} = params;
|
||||
expect(from).toEqual('Sep 21, 2024 @ 20:49:13.452');
|
||||
expect(to).toEqual('Sep 21, 2024 @ 20:49:13.452');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using winlog schema to layout the graph', () => {
|
||||
beforeEach(() => {
|
||||
dispatchTree(generatedTreeMetadata.formattedTree, winlogSourceSchema);
|
||||
|
|
|
@ -12,6 +12,7 @@ import * as treeFetcherParameters from '../../models/tree_fetcher_parameters';
|
|||
import * as selectors from './selectors';
|
||||
import * as nodeEventsInCategoryModel from './node_events_in_category_model';
|
||||
import * as nodeDataModel from '../../models/node_data';
|
||||
import { normalizeTimeRange } from '../../../common/utils/normalize_time_range';
|
||||
import { initialAnalyzerState, immerCase } from '../helpers';
|
||||
import { appReceivedNewExternalProperties } from '../actions';
|
||||
import {
|
||||
|
@ -29,6 +30,7 @@ import {
|
|||
appRequestedCurrentRelatedEventData,
|
||||
serverReturnedCurrentRelatedEventData,
|
||||
serverFailedToReturnCurrentRelatedEventData,
|
||||
userOverrodeDateRange,
|
||||
} from './action';
|
||||
|
||||
export const dataReducer = reducerWithInitialState(initialAnalyzerState)
|
||||
|
@ -247,6 +249,28 @@ export const dataReducer = reducerWithInitialState(initialAnalyzerState)
|
|||
return draft;
|
||||
})
|
||||
)
|
||||
.withHandling(
|
||||
immerCase(userOverrodeDateRange, (draft, { id, timeRange: { from, to } }) => {
|
||||
if (from && to) {
|
||||
const state: Draft<DataState> = draft[id].data;
|
||||
if (state.tree?.currentParameters !== undefined) {
|
||||
state.tree = {
|
||||
...state.tree,
|
||||
currentParameters: {
|
||||
...state.tree.currentParameters,
|
||||
filters: {
|
||||
from,
|
||||
to,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
const normalizedTimeRange = normalizeTimeRange({ from, to });
|
||||
draft[id].data.overriddenTimeBounds = normalizedTimeRange;
|
||||
}
|
||||
return draft;
|
||||
})
|
||||
)
|
||||
.withHandling(
|
||||
immerCase(serverReturnedCurrentRelatedEventData, (draft, { id, relatedEvent }) => {
|
||||
draft[id].data.currentRelatedEvent = {
|
||||
|
|
|
@ -50,6 +50,10 @@ export function detectedBounds(state: DataState) {
|
|||
return state.detectedBounds;
|
||||
}
|
||||
|
||||
export function overriddenTimeBounds(state: DataState) {
|
||||
return state.overriddenTimeBounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a request was made and it threw an error or returned a failure response code.
|
||||
*/
|
||||
|
@ -631,6 +635,28 @@ export const relatedEventCountOfTypeForNode: (
|
|||
}
|
||||
);
|
||||
|
||||
export const currentAppliedTimeRange = createSelector(
|
||||
(state: DataState) => state.tree?.currentParameters?.filters,
|
||||
(state: DataState) => state.tree?.lastResponse?.parameters?.filters,
|
||||
detectedBounds,
|
||||
overriddenTimeBounds,
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
function (currentFilters, lastFilters, detectedBounds, overriddenTimeBounds) {
|
||||
if (overriddenTimeBounds) {
|
||||
return overriddenTimeBounds;
|
||||
} else if (detectedBounds) {
|
||||
return {
|
||||
from: detectedBounds.from,
|
||||
to: detectedBounds.to,
|
||||
};
|
||||
} else if (lastFilters) {
|
||||
return lastFilters;
|
||||
} else if (currentFilters) {
|
||||
return currentFilters;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Which view should show in the panel, as well as what parameters should be used.
|
||||
* Calculated using the query string
|
||||
|
|
|
@ -43,6 +43,16 @@ export const translation = composeSelectors(cameraStateSelector, cameraSelectors
|
|||
|
||||
export const detectedBounds = composeSelectors(dataStateSelector, dataSelectors.detectedBounds);
|
||||
|
||||
export const overriddenTimeBounds = composeSelectors(
|
||||
dataStateSelector,
|
||||
dataSelectors.overriddenTimeBounds
|
||||
);
|
||||
|
||||
export const currentAppliedTimeRange = composeSelectors(
|
||||
dataStateSelector,
|
||||
dataSelectors.currentAppliedTimeRange
|
||||
);
|
||||
|
||||
/**
|
||||
* A matrix that when applied to a Vector2 converts it from screen coordinates to world coordinates.
|
||||
* See https://en.wikipedia.org/wiki/Orthographic_projection
|
||||
|
|
|
@ -315,6 +315,8 @@ export interface DataState {
|
|||
|
||||
readonly detectedBounds?: TimeFilters;
|
||||
|
||||
readonly overriddenTimeBounds?: TimeFilters;
|
||||
|
||||
readonly tree?: {
|
||||
/**
|
||||
* The parameters passed from the resolver properties
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useCallback, memo } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiPopover, EuiPopoverTitle, EuiSuperDatePicker } from '@elastic/eui';
|
||||
import type { ShortDate } from '@elastic/eui';
|
||||
import { formatDate } from '../../../common/components/super_date_picker';
|
||||
import { StyledEuiButtonIcon } from './styles';
|
||||
import { useColors } from '../use_colors';
|
||||
import * as selectors from '../../store/selectors';
|
||||
import { userOverrodeDateRange } from '../../store/data/action';
|
||||
import type { State } from '../../../common/store/types';
|
||||
|
||||
interface DurationRange {
|
||||
end: ShortDate;
|
||||
label?: string;
|
||||
start: ShortDate;
|
||||
}
|
||||
|
||||
const emptyRanges: DurationRange[] = [];
|
||||
|
||||
const nodeLegendButtonTitle = i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.datePickerButtonTitle',
|
||||
{
|
||||
defaultMessage: 'Date Range Selection',
|
||||
}
|
||||
);
|
||||
|
||||
const dateRangeDescription = i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.datePicker',
|
||||
{
|
||||
defaultMessage: 'date range selection',
|
||||
}
|
||||
);
|
||||
|
||||
export const DateSelectionButton = memo(
|
||||
({
|
||||
id,
|
||||
closePopover,
|
||||
setActivePopover,
|
||||
isOpen,
|
||||
}: {
|
||||
id: string;
|
||||
closePopover: () => void;
|
||||
setActivePopover: (value: 'datePicker') => void;
|
||||
isOpen: boolean;
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const setAsActivePopover = useCallback(
|
||||
() => setActivePopover('datePicker'),
|
||||
[setActivePopover]
|
||||
);
|
||||
const colorMap = useColors();
|
||||
|
||||
const appliedBounds = useSelector((state: State) => {
|
||||
return selectors.currentAppliedTimeRange(state.analyzer[id]);
|
||||
});
|
||||
|
||||
const onTimeChange = useCallback(
|
||||
({ start, end, isInvalid }) => {
|
||||
if (!isInvalid) {
|
||||
const isQuickSelection = start.includes('now') || end.includes('now');
|
||||
const fromDate = formatDate(start);
|
||||
let toDate = formatDate(end, { roundUp: true });
|
||||
if (isQuickSelection) {
|
||||
if (start === end) {
|
||||
toDate = formatDate('now');
|
||||
} else {
|
||||
toDate = formatDate(end);
|
||||
}
|
||||
}
|
||||
dispatch(userOverrodeDateRange({ id, timeRange: { from: fromDate, to: toDate } }));
|
||||
}
|
||||
},
|
||||
[dispatch, id]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<StyledEuiButtonIcon
|
||||
data-test-subj="resolver:graph-controls:date-picker-button"
|
||||
size="m"
|
||||
title={nodeLegendButtonTitle}
|
||||
aria-label={nodeLegendButtonTitle}
|
||||
onClick={setAsActivePopover}
|
||||
iconType="calendar"
|
||||
$backgroundColor={colorMap.graphControlsBackground}
|
||||
$iconColor={colorMap.graphControls}
|
||||
$borderColor={colorMap.graphControlsBorderColor}
|
||||
/>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopover}
|
||||
anchorPosition="leftCenter"
|
||||
>
|
||||
<EuiPopoverTitle style={{ textTransform: 'uppercase' }}>
|
||||
{dateRangeDescription}
|
||||
</EuiPopoverTitle>
|
||||
<EuiSuperDatePicker
|
||||
onTimeChange={onTimeChange}
|
||||
start={appliedBounds?.from}
|
||||
end={appliedBounds?.to}
|
||||
showUpdateButton={false}
|
||||
recentlyUsedRanges={emptyRanges}
|
||||
width="auto"
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DateSelectionButton.displayName = 'DateSelectionButton';
|
|
@ -5,10 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Simulator } from '../test_utilities/simulator';
|
||||
import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children';
|
||||
import { nudgeAnimationDuration } from '../store/camera/scaling_constants';
|
||||
import '../test_utilities/extend_jest';
|
||||
import { Simulator } from '../../test_utilities/simulator';
|
||||
import { noAncestorsTwoChildren } from '../../data_access_layer/mocks/no_ancestors_two_children';
|
||||
import { nudgeAnimationDuration } from '../../store/camera/scaling_constants';
|
||||
import '../../test_utilities/extend_jest';
|
||||
|
||||
describe('graph controls: when relsover is loaded with an origin node', () => {
|
||||
let simulator: Simulator;
|
|
@ -0,0 +1,246 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useContext, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { EuiRangeProps } from '@elastic/eui';
|
||||
import { EuiPanel, EuiIcon } from '@elastic/eui';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { SideEffectContext } from '../side_effect_context';
|
||||
import type { Vector2 } from '../../types';
|
||||
import * as selectors from '../../store/selectors';
|
||||
import { useColors } from '../use_colors';
|
||||
import {
|
||||
userClickedZoomIn,
|
||||
userClickedZoomOut,
|
||||
userSetZoomLevel,
|
||||
userNudgedCamera,
|
||||
userSetPositionOfCamera,
|
||||
} from '../../store/camera/action';
|
||||
import type { State } from '../../../common/store/types';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
import { SourcererButton } from './sourcerer_selection';
|
||||
import { DateSelectionButton } from './date_picker';
|
||||
import { StyledGraphControls, StyledGraphControlsColumn, StyledEuiRange } from './styles';
|
||||
import { NodeLegend } from './legend';
|
||||
import { SchemaInformation } from './schema';
|
||||
|
||||
export const GraphControls = React.memo(
|
||||
({
|
||||
id,
|
||||
className,
|
||||
}: {
|
||||
/**
|
||||
* Id that identify the scope of analyzer
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* A className string provided by `styled`
|
||||
*/
|
||||
className?: string;
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const scalingFactor = useSelector((state: State) =>
|
||||
selectors.scalingFactor(state.analyzer[id])
|
||||
);
|
||||
const { timestamp } = useContext(SideEffectContext);
|
||||
const isDatePickerAndSourcererDisabled = useIsExperimentalFeatureEnabled(
|
||||
'analyzerDatePickersAndSourcererDisabled'
|
||||
);
|
||||
const [activePopover, setPopover] = useState<
|
||||
null | 'schemaInfo' | 'nodeLegend' | 'sourcererSelection' | 'datePicker'
|
||||
>(null);
|
||||
const colorMap = useColors();
|
||||
|
||||
const setActivePopover = useCallback(
|
||||
(value) => {
|
||||
if (value === activePopover) {
|
||||
setPopover(null);
|
||||
} else {
|
||||
setPopover(value);
|
||||
}
|
||||
},
|
||||
[setPopover, activePopover]
|
||||
);
|
||||
|
||||
const closePopover = useCallback(() => setPopover(null), []);
|
||||
|
||||
const handleZoomAmountChange: EuiRangeProps['onChange'] = useCallback(
|
||||
(event) => {
|
||||
const valueAsNumber = parseFloat(
|
||||
(event as React.ChangeEvent<HTMLInputElement>).target.value
|
||||
);
|
||||
if (isNaN(valueAsNumber) === false) {
|
||||
dispatch(
|
||||
userSetZoomLevel({
|
||||
id,
|
||||
zoomLevel: valueAsNumber,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[dispatch, id]
|
||||
);
|
||||
|
||||
const handleCenterClick = useCallback(() => {
|
||||
dispatch(userSetPositionOfCamera({ id, cameraView: [0, 0] }));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleZoomOutClick = useCallback(() => {
|
||||
dispatch(userClickedZoomOut({ id }));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleZoomInClick = useCallback(() => {
|
||||
dispatch(userClickedZoomIn({ id }));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const [handleNorth, handleEast, handleSouth, handleWest] = useMemo(() => {
|
||||
const directionVectors: readonly Vector2[] = [
|
||||
[0, 1],
|
||||
[1, 0],
|
||||
[0, -1],
|
||||
[-1, 0],
|
||||
];
|
||||
return directionVectors.map((direction) => {
|
||||
return () => {
|
||||
dispatch(userNudgedCamera({ id, direction, time: timestamp() }));
|
||||
};
|
||||
});
|
||||
}, [dispatch, timestamp, id]);
|
||||
|
||||
/* eslint-disable react/button-has-type */
|
||||
return (
|
||||
<StyledGraphControls
|
||||
className={className}
|
||||
$iconColor={colorMap.graphControls}
|
||||
data-test-subj="resolver:graph-controls"
|
||||
>
|
||||
<StyledGraphControlsColumn>
|
||||
<SchemaInformation
|
||||
id={id}
|
||||
closePopover={closePopover}
|
||||
isOpen={activePopover === 'schemaInfo'}
|
||||
setActivePopover={setActivePopover}
|
||||
/>
|
||||
<NodeLegend
|
||||
id={id}
|
||||
closePopover={closePopover}
|
||||
isOpen={activePopover === 'nodeLegend'}
|
||||
setActivePopover={setActivePopover}
|
||||
/>
|
||||
{!isDatePickerAndSourcererDisabled ? (
|
||||
<>
|
||||
<SourcererButton
|
||||
id={id}
|
||||
closePopover={closePopover}
|
||||
isOpen={activePopover === 'sourcererSelection'}
|
||||
setActivePopover={setActivePopover}
|
||||
/>
|
||||
<DateSelectionButton
|
||||
id={id}
|
||||
closePopover={closePopover}
|
||||
isOpen={activePopover === 'datePicker'}
|
||||
setActivePopover={setActivePopover}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</StyledGraphControlsColumn>
|
||||
<StyledGraphControlsColumn>
|
||||
<EuiPanel className="panning-controls" paddingSize="none" hasBorder>
|
||||
<div className="panning-controls-top">
|
||||
<button
|
||||
className="north-button"
|
||||
data-test-subj="resolver:graph-controls:north-button"
|
||||
title={i18n.translate('xpack.securitySolution.resolver.graphControls.north', {
|
||||
defaultMessage: 'North',
|
||||
})}
|
||||
onClick={handleNorth}
|
||||
>
|
||||
<EuiIcon type="arrowUp" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="panning-controls-middle">
|
||||
<button
|
||||
className="west-button"
|
||||
data-test-subj="resolver:graph-controls:west-button"
|
||||
title={i18n.translate('xpack.securitySolution.resolver.graphControls.west', {
|
||||
defaultMessage: 'West',
|
||||
})}
|
||||
onClick={handleWest}
|
||||
>
|
||||
<EuiIcon type="arrowLeft" />
|
||||
</button>
|
||||
<button
|
||||
className="center-button"
|
||||
data-test-subj="resolver:graph-controls:center-button"
|
||||
title={i18n.translate('xpack.securitySolution.resolver.graphControls.center', {
|
||||
defaultMessage: 'Center',
|
||||
})}
|
||||
onClick={handleCenterClick}
|
||||
>
|
||||
<EuiIcon type="bullseye" />
|
||||
</button>
|
||||
<button
|
||||
className="east-button"
|
||||
data-test-subj="resolver:graph-controls:east-button"
|
||||
title={i18n.translate('xpack.securitySolution.resolver.graphControls.east', {
|
||||
defaultMessage: 'East',
|
||||
})}
|
||||
onClick={handleEast}
|
||||
>
|
||||
<EuiIcon type="arrowRight" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="panning-controls-bottom">
|
||||
<button
|
||||
className="south-button"
|
||||
data-test-subj="resolver:graph-controls:south-button"
|
||||
title={i18n.translate('xpack.securitySolution.resolver.graphControls.south', {
|
||||
defaultMessage: 'South',
|
||||
})}
|
||||
onClick={handleSouth}
|
||||
>
|
||||
<EuiIcon type="arrowDown" />
|
||||
</button>
|
||||
</div>
|
||||
</EuiPanel>
|
||||
<EuiPanel className="zoom-controls" paddingSize="none" hasBorder>
|
||||
<button
|
||||
title={i18n.translate('xpack.securitySolution.resolver.graphControls.zoomIn', {
|
||||
defaultMessage: 'Zoom In',
|
||||
})}
|
||||
data-test-subj="resolver:graph-controls:zoom-in"
|
||||
onClick={handleZoomInClick}
|
||||
>
|
||||
<EuiIcon type="plusInCircle" />
|
||||
</button>
|
||||
<StyledEuiRange
|
||||
className="zoom-slider"
|
||||
data-test-subj="resolver:graph-controls:zoom-slider"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={scalingFactor}
|
||||
onChange={handleZoomAmountChange}
|
||||
/>
|
||||
<button
|
||||
title={i18n.translate('xpack.securitySolution.resolver.graphControls.zoomOut', {
|
||||
defaultMessage: 'Zoom Out',
|
||||
})}
|
||||
data-test-subj="resolver:graph-controls:zoom-out"
|
||||
onClick={handleZoomOutClick}
|
||||
>
|
||||
<EuiIcon type="minusInCircle" />
|
||||
</button>
|
||||
</EuiPanel>
|
||||
</StyledGraphControlsColumn>
|
||||
</StyledGraphControls>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
GraphControls.displayName = 'GraphControls';
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiPopover, EuiPopoverTitle } from '@elastic/eui';
|
||||
import { useColors } from '../use_colors';
|
||||
import { StyledDescriptionList } from '../panels/styles';
|
||||
import { CubeForProcess } from '../panels/cube_for_process';
|
||||
import { GeneratedText } from '../generated_text';
|
||||
import {
|
||||
StyledEuiDescriptionListTitle,
|
||||
StyledEuiDescriptionListDescription,
|
||||
StyledEuiButtonIcon,
|
||||
COLUMN_WIDTH,
|
||||
} from './styles';
|
||||
|
||||
// This component defines the cube legend that allows users to identify the meaning of the cubes
|
||||
// Should be updated to be dynamic if and when non process based resolvers are possible
|
||||
export const NodeLegend = ({
|
||||
id,
|
||||
closePopover,
|
||||
setActivePopover,
|
||||
isOpen,
|
||||
}: {
|
||||
id: string;
|
||||
closePopover: () => void;
|
||||
setActivePopover: (value: 'nodeLegend') => void;
|
||||
isOpen: boolean;
|
||||
}) => {
|
||||
const setAsActivePopover = useCallback(() => setActivePopover('nodeLegend'), [setActivePopover]);
|
||||
const colorMap = useColors();
|
||||
|
||||
const nodeLegendButtonTitle = i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.nodeLegendButtonTitle',
|
||||
{
|
||||
defaultMessage: 'Node Legend',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<StyledEuiButtonIcon
|
||||
data-test-subj="resolver:graph-controls:node-legend-button"
|
||||
size="m"
|
||||
title={nodeLegendButtonTitle}
|
||||
aria-label={nodeLegendButtonTitle}
|
||||
onClick={setAsActivePopover}
|
||||
iconType="node"
|
||||
$backgroundColor={colorMap.graphControlsBackground}
|
||||
$iconColor={colorMap.graphControls}
|
||||
$borderColor={colorMap.graphControlsBorderColor}
|
||||
/>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopover}
|
||||
anchorPosition="leftCenter"
|
||||
>
|
||||
<EuiPopoverTitle style={{ textTransform: 'uppercase' }}>
|
||||
{i18n.translate('xpack.securitySolution.resolver.graphControls.nodeLegend', {
|
||||
defaultMessage: 'legend',
|
||||
})}
|
||||
</EuiPopoverTitle>
|
||||
<div
|
||||
// Limit the width based on UX design
|
||||
style={{ maxWidth: '212px' }}
|
||||
>
|
||||
<StyledDescriptionList
|
||||
data-test-subj="resolver:graph-controls:node-legend"
|
||||
type="column"
|
||||
columnWidths={COLUMN_WIDTH}
|
||||
align="left"
|
||||
compressed
|
||||
>
|
||||
<>
|
||||
<StyledEuiDescriptionListTitle data-test-subj="resolver:graph-controls:node-legend:title">
|
||||
<CubeForProcess
|
||||
id={id}
|
||||
size="2.5em"
|
||||
data-test-subj="resolver:node-detail:title-icon"
|
||||
state="running"
|
||||
/>
|
||||
</StyledEuiDescriptionListTitle>
|
||||
<StyledEuiDescriptionListDescription data-test-subj="resolver:graph-controls:node-legend:description">
|
||||
<GeneratedText>
|
||||
{i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.runningProcessCube',
|
||||
{
|
||||
defaultMessage: 'Running Process',
|
||||
}
|
||||
)}
|
||||
</GeneratedText>
|
||||
</StyledEuiDescriptionListDescription>
|
||||
<StyledEuiDescriptionListTitle data-test-subj="resolver:graph-controls:node-legend:title">
|
||||
<CubeForProcess
|
||||
id={id}
|
||||
size="2.5em"
|
||||
data-test-subj="resolver:node-detail:title-icon"
|
||||
state="terminated"
|
||||
/>
|
||||
</StyledEuiDescriptionListTitle>
|
||||
<StyledEuiDescriptionListDescription data-test-subj="resolver:graph-controls:node-legend:description">
|
||||
<GeneratedText>
|
||||
{i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.terminatedProcessCube',
|
||||
{
|
||||
defaultMessage: 'Terminated Process',
|
||||
}
|
||||
)}
|
||||
</GeneratedText>
|
||||
</StyledEuiDescriptionListDescription>
|
||||
<StyledEuiDescriptionListTitle data-test-subj="resolver:graph-controls:node-legend:title">
|
||||
<CubeForProcess
|
||||
id={id}
|
||||
size="2.5em"
|
||||
data-test-subj="resolver:node-detail:title-icon"
|
||||
state="loading"
|
||||
/>
|
||||
</StyledEuiDescriptionListTitle>
|
||||
<StyledEuiDescriptionListDescription data-test-subj="resolver:graph-controls:node-legend:description">
|
||||
<GeneratedText>
|
||||
{i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.currentlyLoadingCube',
|
||||
{
|
||||
defaultMessage: 'Loading Process',
|
||||
}
|
||||
)}
|
||||
</GeneratedText>
|
||||
</StyledEuiDescriptionListDescription>
|
||||
<StyledEuiDescriptionListTitle data-test-subj="resolver:graph-controls:node-legend:title">
|
||||
<CubeForProcess
|
||||
id={id}
|
||||
size="2.5em"
|
||||
data-test-subj="resolver:node-detail:title-icon"
|
||||
state="error"
|
||||
/>
|
||||
</StyledEuiDescriptionListTitle>
|
||||
<StyledEuiDescriptionListDescription data-test-subj="resolver:graph-controls:node-legend:description">
|
||||
<GeneratedText>
|
||||
{i18n.translate('xpack.securitySolution.resolver.graphControls.errorCube', {
|
||||
defaultMessage: 'Error Process',
|
||||
})}
|
||||
</GeneratedText>
|
||||
</StyledEuiDescriptionListDescription>
|
||||
</>
|
||||
</StyledDescriptionList>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiIconTip,
|
||||
EuiDescriptionListDescription,
|
||||
} from '@elastic/eui';
|
||||
import { useSelector } from 'react-redux';
|
||||
import * as selectors from '../../store/selectors';
|
||||
import { useColors } from '../use_colors';
|
||||
import { StyledDescriptionList } from '../panels/styles';
|
||||
import { GeneratedText } from '../generated_text';
|
||||
import type { State } from '../../../common/store/types';
|
||||
import { StyledEuiDescriptionListTitle, StyledEuiButtonIcon, COLUMN_WIDTH } from './styles';
|
||||
|
||||
export const SchemaInformation = ({
|
||||
id,
|
||||
closePopover,
|
||||
setActivePopover,
|
||||
isOpen,
|
||||
}: {
|
||||
id: string;
|
||||
closePopover: () => void;
|
||||
setActivePopover: (value: 'schemaInfo' | null) => void;
|
||||
isOpen: boolean;
|
||||
}) => {
|
||||
const colorMap = useColors();
|
||||
const sourceAndSchema = useSelector((state: State) =>
|
||||
selectors.resolverTreeSourceAndSchema(state.analyzer[id])
|
||||
);
|
||||
const setAsActivePopover = useCallback(() => setActivePopover('schemaInfo'), [setActivePopover]);
|
||||
|
||||
const schemaInfoButtonTitle = i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.schemaInfoButtonTitle',
|
||||
{
|
||||
defaultMessage: 'Schema Information',
|
||||
}
|
||||
);
|
||||
|
||||
const unknownSchemaValue = i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.unknownSchemaValue',
|
||||
{
|
||||
defaultMessage: 'Unknown',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<StyledEuiButtonIcon
|
||||
data-test-subj="resolver:graph-controls:schema-info-button"
|
||||
size="m"
|
||||
title={schemaInfoButtonTitle}
|
||||
aria-label={schemaInfoButtonTitle}
|
||||
onClick={setAsActivePopover}
|
||||
iconType="iInCircle"
|
||||
$backgroundColor={colorMap.graphControlsBackground}
|
||||
$iconColor={colorMap.graphControls}
|
||||
$borderColor={colorMap.graphControlsBorderColor}
|
||||
/>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopover}
|
||||
anchorPosition="leftCenter"
|
||||
>
|
||||
<EuiPopoverTitle style={{ textTransform: 'uppercase' }}>
|
||||
{i18n.translate('xpack.securitySolution.resolver.graphControls.schemaInfoTitle', {
|
||||
defaultMessage: 'process tree',
|
||||
})}
|
||||
<EuiIconTip
|
||||
content={i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.schemaInfoTooltip',
|
||||
{
|
||||
defaultMessage: 'These are the fields used to create the process tree',
|
||||
}
|
||||
)}
|
||||
position="right"
|
||||
/>
|
||||
</EuiPopoverTitle>
|
||||
<div
|
||||
// Limit the width based on UX design
|
||||
style={{ maxWidth: '268px' }}
|
||||
>
|
||||
<StyledDescriptionList
|
||||
data-test-subj="resolver:graph-controls:schema-info"
|
||||
type="column"
|
||||
columnWidths={COLUMN_WIDTH}
|
||||
align="left"
|
||||
compressed
|
||||
>
|
||||
<>
|
||||
<StyledEuiDescriptionListTitle data-test-subj="resolver:graph-controls:schema-info:title">
|
||||
{i18n.translate('xpack.securitySolution.resolver.graphControls.schemaSource', {
|
||||
defaultMessage: 'source',
|
||||
})}
|
||||
</StyledEuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="resolver:graph-controls:schema-info:description">
|
||||
<GeneratedText>{sourceAndSchema?.dataSource ?? unknownSchemaValue}</GeneratedText>
|
||||
</EuiDescriptionListDescription>
|
||||
<StyledEuiDescriptionListTitle data-test-subj="resolver:graph-controls:schema-info:title">
|
||||
{i18n.translate('xpack.securitySolution.resolver.graphControls.schemaID', {
|
||||
defaultMessage: 'id',
|
||||
})}
|
||||
</StyledEuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="resolver:graph-controls:schema-info:description">
|
||||
<GeneratedText>{sourceAndSchema?.schema.id ?? unknownSchemaValue}</GeneratedText>
|
||||
</EuiDescriptionListDescription>
|
||||
<StyledEuiDescriptionListTitle data-test-subj="resolver:graph-controls:schema-info:title">
|
||||
{i18n.translate('xpack.securitySolution.resolver.graphControls.schemaEdge', {
|
||||
defaultMessage: 'edge',
|
||||
})}
|
||||
</StyledEuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="resolver:graph-controls:schema-info:description">
|
||||
<GeneratedText>{sourceAndSchema?.schema.parent ?? unknownSchemaValue}</GeneratedText>
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
</StyledDescriptionList>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useCallback, memo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiPopover } from '@elastic/eui';
|
||||
import { StyledEuiButtonIcon } from './styles';
|
||||
import { useColors } from '../use_colors';
|
||||
import { Sourcerer } from '../../../common/components/sourcerer';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
|
||||
const nodeLegendButtonTitle = i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.sourcererButtonTitle',
|
||||
{
|
||||
defaultMessage: 'Data View Selection',
|
||||
}
|
||||
);
|
||||
|
||||
export const SourcererButton = memo(
|
||||
({
|
||||
id,
|
||||
closePopover,
|
||||
setActivePopover,
|
||||
isOpen,
|
||||
}: {
|
||||
id: string;
|
||||
closePopover: () => void;
|
||||
setActivePopover: (value: 'sourcererSelection') => void;
|
||||
isOpen: boolean;
|
||||
}) => {
|
||||
const setAsActivePopover = useCallback(
|
||||
() => setActivePopover('sourcererSelection'),
|
||||
[setActivePopover]
|
||||
);
|
||||
const colorMap = useColors();
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<StyledEuiButtonIcon
|
||||
data-test-subj="resolver:graph-controls:sourcerer-button"
|
||||
size="m"
|
||||
title={nodeLegendButtonTitle}
|
||||
aria-label={nodeLegendButtonTitle}
|
||||
onClick={setAsActivePopover}
|
||||
iconType="indexSettings"
|
||||
$backgroundColor={colorMap.graphControlsBackground}
|
||||
$iconColor={colorMap.graphControls}
|
||||
$borderColor={colorMap.graphControlsBorderColor}
|
||||
/>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopover}
|
||||
anchorPosition="leftCenter"
|
||||
>
|
||||
<Sourcerer scope={SourcererScopeName.analyzer} />
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SourcererButton.displayName = 'SourcererButton';
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { EuiRangeProps } from '@elastic/eui';
|
||||
import {
|
||||
EuiRange,
|
||||
EuiButtonIcon,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const COLUMN_WIDTH = ['fit-content(10em)', 'auto'];
|
||||
|
||||
// EuiRange is currently only horizontally positioned. This reorients the track to a vertical position
|
||||
export const StyledEuiRange = styled(EuiRange)<EuiRangeProps>`
|
||||
& .euiRangeTrack:after {
|
||||
left: -65px;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
`;
|
||||
export interface StyledGraphControlProps {
|
||||
$backgroundColor: string;
|
||||
$iconColor: string;
|
||||
$borderColor: string;
|
||||
}
|
||||
|
||||
export const StyledGraphControlsColumn = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:not(last-of-type) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledEuiDescriptionListTitle = styled(EuiDescriptionListTitle)`
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
export const StyledEuiDescriptionListDescription = styled(EuiDescriptionListDescription)`
|
||||
lineheight: '2.2em'; // lineHeight to align center vertically
|
||||
`;
|
||||
|
||||
export const StyledEuiButtonIcon = styled(EuiButtonIcon)<StyledGraphControlProps>`
|
||||
background-color: ${(props) => props.$backgroundColor};
|
||||
color: ${(props) => props.$iconColor};
|
||||
border-color: ${(props) => props.$borderColor};
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 4px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
&:not(last-of-type) {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledGraphControls = styled.div<Partial<StyledGraphControlProps>>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background-color: transparent;
|
||||
color: ${(props) => props.$iconColor};
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 5px 0px;
|
||||
|
||||
.zoom-slider {
|
||||
width: 20px;
|
||||
height: 150px;
|
||||
margin: 5px 0px 2px 0px;
|
||||
|
||||
input[type='range'] {
|
||||
width: 150px;
|
||||
height: 20px;
|
||||
transform-origin: 75px 75px;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
.panning-controls {
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
|
@ -1,558 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useContext, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { EuiRangeProps } from '@elastic/eui';
|
||||
import {
|
||||
EuiRange,
|
||||
EuiPanel,
|
||||
EuiIcon,
|
||||
EuiButtonIcon,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiIconTip,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
} from '@elastic/eui';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { SideEffectContext } from './side_effect_context';
|
||||
import type { Vector2 } from '../types';
|
||||
import * as selectors from '../store/selectors';
|
||||
import { useColors } from './use_colors';
|
||||
import { StyledDescriptionList } from './panels/styles';
|
||||
import { CubeForProcess } from './panels/cube_for_process';
|
||||
import { GeneratedText } from './generated_text';
|
||||
import {
|
||||
userClickedZoomIn,
|
||||
userClickedZoomOut,
|
||||
userSetZoomLevel,
|
||||
userNudgedCamera,
|
||||
userSetPositionOfCamera,
|
||||
} from '../store/camera/action';
|
||||
import type { State } from '../../common/store/types';
|
||||
|
||||
// EuiRange is currently only horizontally positioned. This reorients the track to a vertical position
|
||||
const StyledEuiRange = styled(EuiRange)<EuiRangeProps>`
|
||||
& .euiRangeTrack:after {
|
||||
left: -65px;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
`;
|
||||
interface StyledGraphControlProps {
|
||||
$backgroundColor: string;
|
||||
$iconColor: string;
|
||||
$borderColor: string;
|
||||
}
|
||||
|
||||
const StyledGraphControlsColumn = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:not(last-of-type) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
const COLUMN_WIDTH = ['fit-content(10em)', 'auto'];
|
||||
|
||||
const StyledEuiDescriptionListTitle = styled(EuiDescriptionListTitle)`
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const StyledEuiDescriptionListDescription = styled(EuiDescriptionListDescription)`
|
||||
lineheight: '2.2em'; // lineHeight to align center vertically
|
||||
`;
|
||||
|
||||
const StyledEuiButtonIcon = styled(EuiButtonIcon)<StyledGraphControlProps>`
|
||||
background-color: ${(props) => props.$backgroundColor};
|
||||
color: ${(props) => props.$iconColor};
|
||||
border-color: ${(props) => props.$borderColor};
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 4px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
&:not(last-of-type) {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledGraphControls = styled.div<Partial<StyledGraphControlProps>>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background-color: transparent;
|
||||
color: ${(props) => props.$iconColor};
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 5px 0px;
|
||||
|
||||
.zoom-slider {
|
||||
width: 20px;
|
||||
height: 150px;
|
||||
margin: 5px 0px 2px 0px;
|
||||
|
||||
input[type='range'] {
|
||||
width: 150px;
|
||||
height: 20px;
|
||||
transform-origin: 75px 75px;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
.panning-controls {
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
/**
|
||||
* Controls for zooming, panning, and centering in Resolver
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
export const GraphControls = React.memo(
|
||||
({
|
||||
id,
|
||||
className,
|
||||
}: {
|
||||
/**
|
||||
* Id that identify the scope of analyzer
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* A className string provided by `styled`
|
||||
*/
|
||||
className?: string;
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const scalingFactor = useSelector((state: State) =>
|
||||
selectors.scalingFactor(state.analyzer[id])
|
||||
);
|
||||
const { timestamp } = useContext(SideEffectContext);
|
||||
const [activePopover, setPopover] = useState<null | 'schemaInfo' | 'nodeLegend'>(null);
|
||||
const colorMap = useColors();
|
||||
|
||||
const setActivePopover = useCallback(
|
||||
(value) => {
|
||||
if (value === activePopover) {
|
||||
setPopover(null);
|
||||
} else {
|
||||
setPopover(value);
|
||||
}
|
||||
},
|
||||
[setPopover, activePopover]
|
||||
);
|
||||
|
||||
const closePopover = useCallback(() => setPopover(null), []);
|
||||
|
||||
const handleZoomAmountChange: EuiRangeProps['onChange'] = useCallback(
|
||||
(event) => {
|
||||
const valueAsNumber = parseFloat(
|
||||
(event as React.ChangeEvent<HTMLInputElement>).target.value
|
||||
);
|
||||
if (isNaN(valueAsNumber) === false) {
|
||||
dispatch(
|
||||
userSetZoomLevel({
|
||||
id,
|
||||
zoomLevel: valueAsNumber,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[dispatch, id]
|
||||
);
|
||||
|
||||
const handleCenterClick = useCallback(() => {
|
||||
dispatch(userSetPositionOfCamera({ id, cameraView: [0, 0] }));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleZoomOutClick = useCallback(() => {
|
||||
dispatch(userClickedZoomOut({ id }));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleZoomInClick = useCallback(() => {
|
||||
dispatch(userClickedZoomIn({ id }));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const [handleNorth, handleEast, handleSouth, handleWest] = useMemo(() => {
|
||||
const directionVectors: readonly Vector2[] = [
|
||||
[0, 1],
|
||||
[1, 0],
|
||||
[0, -1],
|
||||
[-1, 0],
|
||||
];
|
||||
return directionVectors.map((direction) => {
|
||||
return () => {
|
||||
dispatch(userNudgedCamera({ id, direction, time: timestamp() }));
|
||||
};
|
||||
});
|
||||
}, [dispatch, timestamp, id]);
|
||||
|
||||
/* eslint-disable react/button-has-type */
|
||||
return (
|
||||
<StyledGraphControls
|
||||
className={className}
|
||||
$iconColor={colorMap.graphControls}
|
||||
data-test-subj="resolver:graph-controls"
|
||||
>
|
||||
<StyledGraphControlsColumn>
|
||||
<SchemaInformation
|
||||
id={id}
|
||||
closePopover={closePopover}
|
||||
isOpen={activePopover === 'schemaInfo'}
|
||||
setActivePopover={setActivePopover}
|
||||
/>
|
||||
<NodeLegend
|
||||
id={id}
|
||||
closePopover={closePopover}
|
||||
isOpen={activePopover === 'nodeLegend'}
|
||||
setActivePopover={setActivePopover}
|
||||
/>
|
||||
</StyledGraphControlsColumn>
|
||||
<StyledGraphControlsColumn>
|
||||
<EuiPanel className="panning-controls" paddingSize="none" hasBorder>
|
||||
<div className="panning-controls-top">
|
||||
<button
|
||||
className="north-button"
|
||||
data-test-subj="resolver:graph-controls:north-button"
|
||||
title={i18n.translate('xpack.securitySolution.resolver.graphControls.north', {
|
||||
defaultMessage: 'North',
|
||||
})}
|
||||
onClick={handleNorth}
|
||||
>
|
||||
<EuiIcon type="arrowUp" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="panning-controls-middle">
|
||||
<button
|
||||
className="west-button"
|
||||
data-test-subj="resolver:graph-controls:west-button"
|
||||
title={i18n.translate('xpack.securitySolution.resolver.graphControls.west', {
|
||||
defaultMessage: 'West',
|
||||
})}
|
||||
onClick={handleWest}
|
||||
>
|
||||
<EuiIcon type="arrowLeft" />
|
||||
</button>
|
||||
<button
|
||||
className="center-button"
|
||||
data-test-subj="resolver:graph-controls:center-button"
|
||||
title={i18n.translate('xpack.securitySolution.resolver.graphControls.center', {
|
||||
defaultMessage: 'Center',
|
||||
})}
|
||||
onClick={handleCenterClick}
|
||||
>
|
||||
<EuiIcon type="bullseye" />
|
||||
</button>
|
||||
<button
|
||||
className="east-button"
|
||||
data-test-subj="resolver:graph-controls:east-button"
|
||||
title={i18n.translate('xpack.securitySolution.resolver.graphControls.east', {
|
||||
defaultMessage: 'East',
|
||||
})}
|
||||
onClick={handleEast}
|
||||
>
|
||||
<EuiIcon type="arrowRight" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="panning-controls-bottom">
|
||||
<button
|
||||
className="south-button"
|
||||
data-test-subj="resolver:graph-controls:south-button"
|
||||
title={i18n.translate('xpack.securitySolution.resolver.graphControls.south', {
|
||||
defaultMessage: 'South',
|
||||
})}
|
||||
onClick={handleSouth}
|
||||
>
|
||||
<EuiIcon type="arrowDown" />
|
||||
</button>
|
||||
</div>
|
||||
</EuiPanel>
|
||||
<EuiPanel className="zoom-controls" paddingSize="none" hasBorder>
|
||||
<button
|
||||
title={i18n.translate('xpack.securitySolution.resolver.graphControls.zoomIn', {
|
||||
defaultMessage: 'Zoom In',
|
||||
})}
|
||||
data-test-subj="resolver:graph-controls:zoom-in"
|
||||
onClick={handleZoomInClick}
|
||||
>
|
||||
<EuiIcon type="plusInCircle" />
|
||||
</button>
|
||||
<StyledEuiRange
|
||||
className="zoom-slider"
|
||||
data-test-subj="resolver:graph-controls:zoom-slider"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={scalingFactor}
|
||||
onChange={handleZoomAmountChange}
|
||||
/>
|
||||
<button
|
||||
title={i18n.translate('xpack.securitySolution.resolver.graphControls.zoomOut', {
|
||||
defaultMessage: 'Zoom Out',
|
||||
})}
|
||||
data-test-subj="resolver:graph-controls:zoom-out"
|
||||
onClick={handleZoomOutClick}
|
||||
>
|
||||
<EuiIcon type="minusInCircle" />
|
||||
</button>
|
||||
</EuiPanel>
|
||||
</StyledGraphControlsColumn>
|
||||
</StyledGraphControls>
|
||||
);
|
||||
/* eslint-enable react/button-has-type */
|
||||
}
|
||||
);
|
||||
|
||||
const SchemaInformation = ({
|
||||
id,
|
||||
closePopover,
|
||||
setActivePopover,
|
||||
isOpen,
|
||||
}: {
|
||||
id: string;
|
||||
closePopover: () => void;
|
||||
setActivePopover: (value: 'schemaInfo' | null) => void;
|
||||
isOpen: boolean;
|
||||
}) => {
|
||||
const colorMap = useColors();
|
||||
const sourceAndSchema = useSelector((state: State) =>
|
||||
selectors.resolverTreeSourceAndSchema(state.analyzer[id])
|
||||
);
|
||||
const setAsActivePopover = useCallback(() => setActivePopover('schemaInfo'), [setActivePopover]);
|
||||
|
||||
const schemaInfoButtonTitle = i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.schemaInfoButtonTitle',
|
||||
{
|
||||
defaultMessage: 'Schema Information',
|
||||
}
|
||||
);
|
||||
|
||||
const unknownSchemaValue = i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.unknownSchemaValue',
|
||||
{
|
||||
defaultMessage: 'Unknown',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<StyledEuiButtonIcon
|
||||
data-test-subj="resolver:graph-controls:schema-info-button"
|
||||
size="m"
|
||||
title={schemaInfoButtonTitle}
|
||||
aria-label={schemaInfoButtonTitle}
|
||||
onClick={setAsActivePopover}
|
||||
iconType="iInCircle"
|
||||
$backgroundColor={colorMap.graphControlsBackground}
|
||||
$iconColor={colorMap.graphControls}
|
||||
$borderColor={colorMap.graphControlsBorderColor}
|
||||
/>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopover}
|
||||
anchorPosition="leftCenter"
|
||||
>
|
||||
<EuiPopoverTitle style={{ textTransform: 'uppercase' }}>
|
||||
{i18n.translate('xpack.securitySolution.resolver.graphControls.schemaInfoTitle', {
|
||||
defaultMessage: 'process tree',
|
||||
})}
|
||||
<EuiIconTip
|
||||
content={i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.schemaInfoTooltip',
|
||||
{
|
||||
defaultMessage: 'These are the fields used to create the process tree',
|
||||
}
|
||||
)}
|
||||
position="right"
|
||||
/>
|
||||
</EuiPopoverTitle>
|
||||
<div
|
||||
// Limit the width based on UX design
|
||||
style={{ maxWidth: '268px' }}
|
||||
>
|
||||
<StyledDescriptionList
|
||||
data-test-subj="resolver:graph-controls:schema-info"
|
||||
type="column"
|
||||
columnWidths={COLUMN_WIDTH}
|
||||
align="left"
|
||||
compressed
|
||||
>
|
||||
<>
|
||||
<StyledEuiDescriptionListTitle data-test-subj="resolver:graph-controls:schema-info:title">
|
||||
{i18n.translate('xpack.securitySolution.resolver.graphControls.schemaSource', {
|
||||
defaultMessage: 'source',
|
||||
})}
|
||||
</StyledEuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="resolver:graph-controls:schema-info:description">
|
||||
<GeneratedText>{sourceAndSchema?.dataSource ?? unknownSchemaValue}</GeneratedText>
|
||||
</EuiDescriptionListDescription>
|
||||
<StyledEuiDescriptionListTitle data-test-subj="resolver:graph-controls:schema-info:title">
|
||||
{i18n.translate('xpack.securitySolution.resolver.graphControls.schemaID', {
|
||||
defaultMessage: 'id',
|
||||
})}
|
||||
</StyledEuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="resolver:graph-controls:schema-info:description">
|
||||
<GeneratedText>{sourceAndSchema?.schema.id ?? unknownSchemaValue}</GeneratedText>
|
||||
</EuiDescriptionListDescription>
|
||||
<StyledEuiDescriptionListTitle data-test-subj="resolver:graph-controls:schema-info:title">
|
||||
{i18n.translate('xpack.securitySolution.resolver.graphControls.schemaEdge', {
|
||||
defaultMessage: 'edge',
|
||||
})}
|
||||
</StyledEuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription data-test-subj="resolver:graph-controls:schema-info:description">
|
||||
<GeneratedText>{sourceAndSchema?.schema.parent ?? unknownSchemaValue}</GeneratedText>
|
||||
</EuiDescriptionListDescription>
|
||||
</>
|
||||
</StyledDescriptionList>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
||||
// This component defines the cube legend that allows users to identify the meaning of the cubes
|
||||
// Should be updated to be dynamic if and when non process based resolvers are possible
|
||||
const NodeLegend = ({
|
||||
id,
|
||||
closePopover,
|
||||
setActivePopover,
|
||||
isOpen,
|
||||
}: {
|
||||
id: string;
|
||||
closePopover: () => void;
|
||||
setActivePopover: (value: 'nodeLegend') => void;
|
||||
isOpen: boolean;
|
||||
}) => {
|
||||
const setAsActivePopover = useCallback(() => setActivePopover('nodeLegend'), [setActivePopover]);
|
||||
const colorMap = useColors();
|
||||
|
||||
const nodeLegendButtonTitle = i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.nodeLegendButtonTitle',
|
||||
{
|
||||
defaultMessage: 'Node Legend',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<StyledEuiButtonIcon
|
||||
data-test-subj="resolver:graph-controls:node-legend-button"
|
||||
size="m"
|
||||
title={nodeLegendButtonTitle}
|
||||
aria-label={nodeLegendButtonTitle}
|
||||
onClick={setAsActivePopover}
|
||||
iconType="node"
|
||||
$backgroundColor={colorMap.graphControlsBackground}
|
||||
$iconColor={colorMap.graphControls}
|
||||
$borderColor={colorMap.graphControlsBorderColor}
|
||||
/>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopover}
|
||||
anchorPosition="leftCenter"
|
||||
>
|
||||
<EuiPopoverTitle style={{ textTransform: 'uppercase' }}>
|
||||
{i18n.translate('xpack.securitySolution.resolver.graphControls.nodeLegend', {
|
||||
defaultMessage: 'legend',
|
||||
})}
|
||||
</EuiPopoverTitle>
|
||||
<div
|
||||
// Limit the width based on UX design
|
||||
style={{ maxWidth: '212px' }}
|
||||
>
|
||||
<StyledDescriptionList
|
||||
data-test-subj="resolver:graph-controls:node-legend"
|
||||
type="column"
|
||||
columnWidths={COLUMN_WIDTH}
|
||||
align="left"
|
||||
compressed
|
||||
>
|
||||
<>
|
||||
<StyledEuiDescriptionListTitle data-test-subj="resolver:graph-controls:node-legend:title">
|
||||
<CubeForProcess
|
||||
id={id}
|
||||
size="2.5em"
|
||||
data-test-subj="resolver:node-detail:title-icon"
|
||||
state="running"
|
||||
/>
|
||||
</StyledEuiDescriptionListTitle>
|
||||
<StyledEuiDescriptionListDescription data-test-subj="resolver:graph-controls:node-legend:description">
|
||||
<GeneratedText>
|
||||
{i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.runningProcessCube',
|
||||
{
|
||||
defaultMessage: 'Running Process',
|
||||
}
|
||||
)}
|
||||
</GeneratedText>
|
||||
</StyledEuiDescriptionListDescription>
|
||||
<StyledEuiDescriptionListTitle data-test-subj="resolver:graph-controls:node-legend:title">
|
||||
<CubeForProcess
|
||||
id={id}
|
||||
size="2.5em"
|
||||
data-test-subj="resolver:node-detail:title-icon"
|
||||
state="terminated"
|
||||
/>
|
||||
</StyledEuiDescriptionListTitle>
|
||||
<StyledEuiDescriptionListDescription data-test-subj="resolver:graph-controls:node-legend:description">
|
||||
<GeneratedText>
|
||||
{i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.terminatedProcessCube',
|
||||
{
|
||||
defaultMessage: 'Terminated Process',
|
||||
}
|
||||
)}
|
||||
</GeneratedText>
|
||||
</StyledEuiDescriptionListDescription>
|
||||
<StyledEuiDescriptionListTitle data-test-subj="resolver:graph-controls:node-legend:title">
|
||||
<CubeForProcess
|
||||
id={id}
|
||||
size="2.5em"
|
||||
data-test-subj="resolver:node-detail:title-icon"
|
||||
state="loading"
|
||||
/>
|
||||
</StyledEuiDescriptionListTitle>
|
||||
<StyledEuiDescriptionListDescription data-test-subj="resolver:graph-controls:node-legend:description">
|
||||
<GeneratedText>
|
||||
{i18n.translate(
|
||||
'xpack.securitySolution.resolver.graphControls.currentlyLoadingCube',
|
||||
{
|
||||
defaultMessage: 'Loading Process',
|
||||
}
|
||||
)}
|
||||
</GeneratedText>
|
||||
</StyledEuiDescriptionListDescription>
|
||||
<StyledEuiDescriptionListTitle data-test-subj="resolver:graph-controls:node-legend:title">
|
||||
<CubeForProcess
|
||||
id={id}
|
||||
size="2.5em"
|
||||
data-test-subj="resolver:node-detail:title-icon"
|
||||
state="error"
|
||||
/>
|
||||
</StyledEuiDescriptionListTitle>
|
||||
<StyledEuiDescriptionListDescription data-test-subj="resolver:graph-controls:node-legend:description">
|
||||
<GeneratedText>
|
||||
{i18n.translate('xpack.securitySolution.resolver.graphControls.errorCube', {
|
||||
defaultMessage: 'Error Process',
|
||||
})}
|
||||
</GeneratedText>
|
||||
</StyledEuiDescriptionListDescription>
|
||||
</>
|
||||
</StyledDescriptionList>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -46,7 +46,6 @@ interface ProcessTableView {
|
|||
/**
|
||||
* The "default" view for the panel: A list of all the processes currently in the graph.
|
||||
*/
|
||||
// eslint-disable-next-line react/display-name
|
||||
export const NodeList = memo(({ id }: { id: string }) => {
|
||||
const columns = useMemo<Array<EuiBasicTableColumn<ProcessTableView>>>(
|
||||
() => [
|
||||
|
@ -85,6 +84,7 @@ export const NodeList = memo(({ id }: { id: string }) => {
|
|||
const processTableView: ProcessTableView[] = useSelector(
|
||||
useCallback(
|
||||
(state: State) => {
|
||||
// console.log('lol WAT');
|
||||
const { processNodePositions } = selectors.layout(state.analyzer[id]);
|
||||
const view: ProcessTableView[] = [];
|
||||
for (const treeNode of processNodePositions.keys()) {
|
||||
|
@ -139,7 +139,8 @@ export const NodeList = memo(({ id }: { id: string }) => {
|
|||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
NodeList.displayName = 'NodeList';
|
||||
|
||||
const NodeDetailLink = memo(
|
||||
({ id, name, nodeID }: { id: string; name?: string; nodeID: string }) => {
|
||||
const isOrigin = useSelector((state: State) => {
|
||||
|
@ -211,7 +212,8 @@ const NodeDetailLink = memo(
|
|||
}
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
NodeDetailLink.displayName = 'NodeDetailLink';
|
||||
|
||||
const NodeDetailTimestamp = memo(
|
||||
({ eventDate, id }: { eventDate: string | number | undefined; id: string }) => {
|
||||
const formattedDate = useFormattedDate(eventDate);
|
||||
|
@ -235,3 +237,5 @@ const NodeDetailTimestamp = memo(
|
|||
);
|
||||
}
|
||||
);
|
||||
|
||||
NodeDetailTimestamp.displayName = 'NodeDetailTimestamp';
|
||||
|
|
|
@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { useResolverQueryParamCleaner } from './use_resolver_query_params_cleaner';
|
||||
import * as selectors from '../store/selectors';
|
||||
import { EdgeLine } from './edge_line';
|
||||
import { GraphControls } from './graph_controls';
|
||||
import { GraphControls } from './controls';
|
||||
import { ProcessEventDot } from './process_event_dot';
|
||||
import { useCamera } from './use_camera';
|
||||
import { SymbolDefinitions } from './symbol_definitions';
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useLocation, useHistory } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { clearResolver } from '../store/actions';
|
||||
import { parameterName } from '../store/parameter_name';
|
||||
/**
|
||||
* Cleanup any query string keys that were added by this Resolver instance.
|
||||
|
@ -21,6 +23,7 @@ export function useResolverQueryParamCleaner(id: string) {
|
|||
*/
|
||||
const searchRef = useRef<string>();
|
||||
searchRef.current = useLocation().search;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
|
@ -46,6 +49,7 @@ export function useResolverQueryParamCleaner(id: string) {
|
|||
urlSearchParams.delete(oldResolverKey);
|
||||
const relativeURL = { search: urlSearchParams.toString() };
|
||||
history.replace(relativeURL);
|
||||
dispatch(clearResolver({ id }));
|
||||
};
|
||||
}, [resolverKey, history]);
|
||||
}, [resolverKey, history, dispatch, id]);
|
||||
}
|
||||
|
|
|
@ -13,16 +13,10 @@ import {
|
|||
useGlobalFullScreen,
|
||||
useTimelineFullScreen,
|
||||
} from '../../../common/containers/use_full_screen';
|
||||
import {
|
||||
createMockStore,
|
||||
mockGlobalState,
|
||||
mockIndexNames,
|
||||
TestProviders,
|
||||
} from '../../../common/mock';
|
||||
import { createMockStore, mockGlobalState, TestProviders } from '../../../common/mock';
|
||||
import { TimelineId } from '../../../../common/types/timeline';
|
||||
import { GraphOverlay } from '.';
|
||||
import { useStateSyncingActions } from '../../../resolver/view/use_state_syncing_actions';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { TableId } from '@kbn/securitysolution-data-table';
|
||||
|
||||
jest.mock('../../../common/containers/use_full_screen', () => ({
|
||||
|
@ -135,7 +129,7 @@ describe('GraphOverlay', () => {
|
|||
);
|
||||
|
||||
expect(useStateSyncingActionsMock.mock.calls[0][0].indices).toEqual(
|
||||
mockGlobalState.sourcerer.defaultDataView.patternList
|
||||
mockGlobalState.sourcerer.sourcererScopes.analyzer.selectedPatterns
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -177,47 +171,6 @@ describe('GraphOverlay', () => {
|
|||
expect(overlayContainer).toHaveStyleRule('width', '100%');
|
||||
});
|
||||
|
||||
test('it gets index pattern from Timeline data view', () => {
|
||||
const mockedDefaultDataViewPattern = 'default-dataview-pattern';
|
||||
render(
|
||||
<TestProviders
|
||||
store={createMockStore({
|
||||
...mockGlobalState,
|
||||
timeline: {
|
||||
...mockGlobalState.timeline,
|
||||
timelineById: {
|
||||
[timelineId]: {
|
||||
...mockGlobalState.timeline.timelineById[timelineId],
|
||||
graphEventId: 'definitely-not-null',
|
||||
},
|
||||
},
|
||||
},
|
||||
sourcerer: {
|
||||
...mockGlobalState.sourcerer,
|
||||
defaultDataView: {
|
||||
...mockGlobalState.sourcerer.defaultDataView,
|
||||
patternList: [mockedDefaultDataViewPattern],
|
||||
},
|
||||
sourcererScopes: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes,
|
||||
[SourcererScopeName.timeline]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
|
||||
selectedPatterns: mockIndexNames,
|
||||
},
|
||||
},
|
||||
},
|
||||
})}
|
||||
>
|
||||
<GraphOverlay SessionView={<div />} Navigation={<div />} scopeId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(useStateSyncingActionsMock.mock.calls[0][0].indices).toEqual([
|
||||
...mockIndexNames.sort(),
|
||||
mockedDefaultDataViewPattern,
|
||||
]);
|
||||
});
|
||||
|
||||
test('it renders session view controls', () => {
|
||||
(useGlobalFullScreen as jest.Mock).mockReturnValue({
|
||||
globalFullScreen: false,
|
||||
|
|
|
@ -18,7 +18,6 @@ jest.mock('react-router-dom', () => {
|
|||
|
||||
const defaultDataViewPattern = 'test-dataview-patterns';
|
||||
const timelinePattern = 'test-timeline-patterns';
|
||||
const alertsPagePatterns = '.siem-signals-spacename';
|
||||
const pathname = '/alerts';
|
||||
const store = createMockStore({
|
||||
...mockGlobalState,
|
||||
|
@ -30,12 +29,25 @@ const store = createMockStore({
|
|||
},
|
||||
sourcererScopes: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes,
|
||||
[SourcererScopeName.timeline]: {
|
||||
[SourcererScopeName.analyzer]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
|
||||
selectedPatterns: [timelinePattern],
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
...mockGlobalState.inputs,
|
||||
timeline: {
|
||||
...mockGlobalState.inputs.timeline,
|
||||
timerange: {
|
||||
kind: 'relative',
|
||||
fromStr: 'now/d',
|
||||
toStr: 'now/d',
|
||||
from: '2024-01-07T08:20:18.966Z',
|
||||
to: '2024-01-08T08:20:18.966Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
|
@ -44,18 +56,23 @@ const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|||
|
||||
describe('useTimelineDataFilters', () => {
|
||||
describe('on alerts page', () => {
|
||||
it('returns default data view patterns and alerts page patterns when isActiveTimelines is falsy', () => {
|
||||
const isActiveTimelines = false;
|
||||
const { result } = renderHook(() => useTimelineDataFilters(isActiveTimelines), { wrapper });
|
||||
it('uses the same selected patterns throughout the app', () => {
|
||||
const { result } = renderHook(() => useTimelineDataFilters(false), { wrapper });
|
||||
const { result: timelineResult } = renderHook(() => useTimelineDataFilters(true), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current.selectedPatterns).toEqual([alertsPagePatterns, defaultDataViewPattern]);
|
||||
expect(result.current.selectedPatterns).toEqual(timelineResult.current.selectedPatterns);
|
||||
});
|
||||
|
||||
it('returns default data view patterns and timelinePatterns when isActiveTimelines is truthy', () => {
|
||||
const isActiveTimelines = true;
|
||||
const { result } = renderHook(() => useTimelineDataFilters(isActiveTimelines), { wrapper });
|
||||
it('allows the other parts of the query to remain unique', () => {
|
||||
const { result } = renderHook(() => useTimelineDataFilters(false), { wrapper });
|
||||
const { result: timelineResult } = renderHook(() => useTimelineDataFilters(true), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current.selectedPatterns).toEqual([timelinePattern, defaultDataViewPattern]);
|
||||
expect(result.current.from !== timelineResult.current.from).toBeTruthy();
|
||||
expect(result.current.to !== timelineResult.current.to).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
|
||||
import { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { useDeepEqualSelector } from '../../common/hooks/use_selector';
|
||||
import {
|
||||
isLoadingSelector,
|
||||
startSelector,
|
||||
endSelector,
|
||||
} from '../../common/components/super_date_picker/selectors';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
|
||||
import { SourcererScopeName } from '../../common/store/sourcerer/model';
|
||||
import { useSourcererDataView, getScopeFromPath } from '../../common/containers/sourcerer';
|
||||
import { sourcererSelectors } from '../../common/store';
|
||||
|
@ -22,6 +22,9 @@ export function useTimelineDataFilters(isActiveTimelines: boolean) {
|
|||
const getStartSelector = useMemo(() => startSelector(), []);
|
||||
const getEndSelector = useMemo(() => endSelector(), []);
|
||||
const getIsLoadingSelector = useMemo(() => isLoadingSelector(), []);
|
||||
const isDatePickerAndSourcererDisabled = useIsExperimentalFeatureEnabled(
|
||||
'analyzerDatePickersAndSourcererDisabled'
|
||||
);
|
||||
|
||||
const shouldUpdate = useDeepEqualSelector((state) => {
|
||||
if (isActiveTimelines) {
|
||||
|
@ -62,10 +65,21 @@ export function useTimelineDataFilters(isActiveTimelines: boolean) {
|
|||
: [...new Set([...nonTimelinePatterns, ...defaultDataView.patternList])];
|
||||
}, [isActiveTimelines, timelinePatterns, nonTimelinePatterns, defaultDataView.patternList]);
|
||||
|
||||
return {
|
||||
const { selectedPatterns: analyzerPatterns } = useSourcererDataView(SourcererScopeName.analyzer);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
selectedPatterns: isDatePickerAndSourcererDisabled ? selectedPatterns : analyzerPatterns,
|
||||
from,
|
||||
to,
|
||||
shouldUpdate,
|
||||
};
|
||||
}, [
|
||||
selectedPatterns,
|
||||
from,
|
||||
to,
|
||||
shouldUpdate,
|
||||
};
|
||||
isDatePickerAndSourcererDisabled,
|
||||
analyzerPatterns,
|
||||
]);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue