Skip to content

Commit

Permalink
[7.x] [SIEM][CASE] Track unsaved changes (elastic#60925) (elastic#61441)
Browse files Browse the repository at this point in the history
* Hide bottom bar when flyout is open

* Track unchanged saves

* Make function optional

* Show action bar when close flyout

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
cnasikas and elasticmachine authored Mar 26, 2020
1 parent 53f2d16 commit c0245bc
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api';
import { useStateToaster, errorToToaster } from '../../../components/toasters';
import * as i18n from '../translations';
import { ClosureType } from './types';
import { CurrentConfiguration } from '../../../pages/case/components/configure_cases/reducer';

interface PersistCaseConfigure {
connectorId: string;
Expand All @@ -27,14 +28,17 @@ export interface ReturnUseCaseConfigure {
interface UseCaseConfigure {
setConnector: (newConnectorId: string, newConnectorName?: string) => void;
setClosureType?: (newClosureType: ClosureType) => void;
setCurrentConfiguration?: (configuration: CurrentConfiguration) => void;
}

export const useCaseConfigure = ({
setConnector,
setClosureType,
setCurrentConfiguration,
}: UseCaseConfigure): ReturnUseCaseConfigure => {
const [, dispatchToaster] = useStateToaster();
const [loading, setLoading] = useState(true);
const [firstLoad, setFirstLoad] = useState(false);
const [persistLoading, setPersistLoading] = useState(false);
const [version, setVersion] = useState('');

Expand All @@ -54,6 +58,16 @@ export const useCaseConfigure = ({
setClosureType(res.closureType);
}
setVersion(res.version);

if (!firstLoad) {
setFirstLoad(true);
if (setCurrentConfiguration != null) {
setCurrentConfiguration({
connectorId: res.connectorId,
closureType: res.closureType,
});
}
}
}
}
} catch (error) {
Expand Down Expand Up @@ -104,6 +118,12 @@ export const useCaseConfigure = ({
setClosureType(res.closureType);
}
setVersion(res.version);
if (setCurrentConfiguration != null) {
setCurrentConfiguration({
connectorId: res.connectorId,
closureType: res.closureType,
});
}
}
} catch (error) {
if (!didCancel) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useReducer, useCallback, useEffect, useState } from 'react';
import React, {
useReducer,
useCallback,
useEffect,
useState,
Dispatch,
SetStateAction,
} from 'react';
import styled, { css } from 'styled-components';

import {
Expand All @@ -14,8 +21,9 @@ import {
EuiCallOut,
EuiBottomBar,
EuiButtonEmpty,
EuiText,
} from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import { isEmpty, difference } from 'lodash/fp';
import { useKibana } from '../../../../lib/kibana';
import { useConnectors } from '../../../../containers/case/configure/use_connectors';
import { useCaseConfigure } from '../../../../containers/case/configure/use_configure';
Expand All @@ -40,7 +48,7 @@ import { ClosureOptions } from '../configure_cases/closure_options';
import { Mapping } from '../configure_cases/mapping';
import { SectionWrapper } from '../wrappers';
import { navTabs } from '../../../../pages/home/home_navigations';
import { configureCasesReducer, State } from './reducer';
import { configureCasesReducer, State, CurrentConfiguration } from './reducer';
import * as i18n from './translations';

const FormWrapper = styled.div`
Expand All @@ -58,6 +66,7 @@ const initialState: State = {
connectorId: 'none',
closureType: 'close-by-user',
mapping: null,
currentConfiguration: { connectorId: 'none', closureType: 'close-by-user' },
};

const actionTypes: ActionType[] = [
Expand All @@ -83,14 +92,20 @@ const ConfigureCasesComponent: React.FC = () => {
);

const [actionBarVisible, setActionBarVisible] = useState(false);
const [totalConfigurationChanges, setTotalConfigurationChanges] = useState(0);

const handleShowAddFlyout = useCallback(() => setAddFlyoutVisibility(true), []);

const [{ connectorId, closureType, mapping }, dispatch] = useReducer(
const [{ connectorId, closureType, mapping, currentConfiguration }, dispatch] = useReducer(
configureCasesReducer(),
initialState
);

const setCurrentConfiguration = useCallback((configuration: CurrentConfiguration) => {
dispatch({
type: 'setCurrentConfiguration',
currentConfiguration: { ...configuration },
});
}, []);

const setConnectorId = useCallback((newConnectorId: string) => {
dispatch({
type: 'setConnectorId',
Expand All @@ -115,6 +130,7 @@ const ConfigureCasesComponent: React.FC = () => {
const { loading: loadingCaseConfigure, persistLoading, persistCaseConfigure } = useCaseConfigure({
setConnector: setConnectorId,
setClosureType,
setCurrentConfiguration,
});
const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors();

Expand All @@ -137,16 +153,47 @@ const ConfigureCasesComponent: React.FC = () => {
[connectorId, connectors, closureType, mapping]
);

const onChangeConnector = useCallback((newConnectorId: string) => {
setActionBarVisible(true);
setConnectorId(newConnectorId);
const onClickAddConnector = useCallback(() => {
setActionBarVisible(false);
setAddFlyoutVisibility(true);
}, []);

const onChangeClosureType = useCallback((newClosureType: ClosureType) => {
setActionBarVisible(true);
setClosureType(newClosureType);
const onClickUpdateConnector = useCallback(() => {
setActionBarVisible(false);
setEditFlyoutVisibility(true);
}, []);

const handleActionBar = useCallback(() => {
const unsavedChanges = difference(Object.values(currentConfiguration), [
connectorId,
closureType,
]).length;

if (unsavedChanges === 0) {
setActionBarVisible(false);
} else {
setActionBarVisible(true);
}

setTotalConfigurationChanges(unsavedChanges);
}, [currentConfiguration, connectorId, closureType]);

const handleSetAddFlyoutVisibility = useCallback(
(isVisible: boolean) => {
handleActionBar();
setAddFlyoutVisibility(isVisible);
},
[currentConfiguration, connectorId, closureType]
);

const handleSetEditFlyoutVisibility = useCallback(
(isVisible: boolean) => {
handleActionBar();
setEditFlyoutVisibility(isVisible);
},
[currentConfiguration, connectorId, closureType]
);

useEffect(() => {
if (
!isEmpty(connectors) &&
Expand Down Expand Up @@ -188,6 +235,10 @@ const ConfigureCasesComponent: React.FC = () => {
}
}, [connectors, connectorId]);

useEffect(() => {
handleActionBar();
}, [connectors, connectorId, closureType, currentConfiguration]);

return (
<FormWrapper>
{!connectorIsValid && (
Expand All @@ -202,16 +253,16 @@ const ConfigureCasesComponent: React.FC = () => {
connectors={connectors ?? []}
disabled={persistLoading || isLoadingConnectors}
isLoading={isLoadingConnectors}
onChangeConnector={onChangeConnector}
handleShowAddFlyout={handleShowAddFlyout}
onChangeConnector={setConnectorId}
handleShowAddFlyout={onClickAddConnector}
selectedConnector={connectorId}
/>
</SectionWrapper>
<SectionWrapper>
<ClosureOptions
closureTypeSelected={closureType}
disabled={persistLoading || isLoadingConnectors || connectorId === 'none'}
onChangeClosureType={onChangeClosureType}
onChangeClosureType={setClosureType}
/>
</SectionWrapper>
<SectionWrapper>
Expand All @@ -220,12 +271,17 @@ const ConfigureCasesComponent: React.FC = () => {
updateConnectorDisabled={updateConnectorDisabled}
mapping={mapping}
onChangeMapping={setMapping}
setEditFlyoutVisibility={setEditFlyoutVisibility}
setEditFlyoutVisibility={onClickUpdateConnector}
/>
</SectionWrapper>
{actionBarVisible && (
<EuiBottomBar>
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiText>{i18n.UNSAVED_CHANGES(totalConfigurationChanges)}</EuiText>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
Expand Down Expand Up @@ -269,15 +325,17 @@ const ConfigureCasesComponent: React.FC = () => {
>
<ConnectorAddFlyout
addFlyoutVisible={addFlyoutVisible}
setAddFlyoutVisibility={setAddFlyoutVisibility}
setAddFlyoutVisibility={handleSetAddFlyoutVisibility as Dispatch<SetStateAction<boolean>>}
actionTypes={actionTypes}
/>
{editedConnectorItem && (
<ConnectorEditFlyout
key={editedConnectorItem.id}
initialConnector={editedConnectorItem}
editFlyoutVisible={editFlyoutVisible}
setEditFlyoutVisibility={setEditFlyoutVisibility}
setEditFlyoutVisibility={
handleSetEditFlyoutVisibility as Dispatch<SetStateAction<boolean>>
}
/>
)}
</ActionsConnectorsContextProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useCallback } from 'react';
import React from 'react';
import styled from 'styled-components';

import {
Expand All @@ -25,7 +25,7 @@ interface MappingProps {
updateConnectorDisabled: boolean;
mapping: CasesConfigurationMapping[] | null;
onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void;
setEditFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
setEditFlyoutVisibility: () => void;
}

const EuiButtonEmptyExtended = styled(EuiButtonEmpty)`
Expand All @@ -40,8 +40,6 @@ const MappingComponent: React.FC<MappingProps> = ({
onChangeMapping,
setEditFlyoutVisibility,
}) => {
const onClick = useCallback(() => setEditFlyoutVisibility(true), []);

return (
<EuiDescribedFormGroup
fullWidth
Expand All @@ -51,7 +49,10 @@ const MappingComponent: React.FC<MappingProps> = ({
<EuiFormRow fullWidth>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false} className="euiFormLabel">
<EuiButtonEmptyExtended onClick={onClick} disabled={updateConnectorDisabled}>
<EuiButtonEmptyExtended
onClick={setEditFlyoutVisibility}
disabled={updateConnectorDisabled}
>
{i18n.UPDATE_CONNECTOR}
</EuiButtonEmptyExtended>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,19 @@ export interface State {
mapping: CasesConfigurationMapping[] | null;
connectorId: string;
closureType: ClosureType;
currentConfiguration: CurrentConfiguration;
}

export interface CurrentConfiguration {
connectorId: State['connectorId'];
closureType: State['closureType'];
}

export type Action =
| {
type: 'setCurrentConfiguration';
currentConfiguration: CurrentConfiguration;
}
| {
type: 'setConnectorId';
connectorId: string;
Expand All @@ -31,6 +41,12 @@ export type Action =

export const configureCasesReducer = () => (state: State, action: Action) => {
switch (action.type) {
case 'setCurrentConfiguration': {
return {
...state,
currentConfiguration: { ...action.currentConfiguration },
};
}
case 'setConnectorId': {
return {
...state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,10 @@ export const FIELD_MAPPING_FIELD_COMMENTS = i18n.translate(
export const UPDATE_CONNECTOR = i18n.translate('xpack.siem.case.configureCases.updateConnector', {
defaultMessage: 'Update connector',
});

export const UNSAVED_CHANGES = (unsavedChanges: number): string => {
return i18n.translate('xpack.siem.case.configureCases.unsavedChanges', {
values: { unsavedChanges },
defaultMessage: '{unsavedChanges} unsaved changes',
});
};

0 comments on commit c0245bc

Please sign in to comment.