Skip to content

Commit

Permalink
Adds empty page with an add button that opens flyout. Alos added rout…
Browse files Browse the repository at this point in the history
…e and path management
  • Loading branch information
dasansol92 committed Apr 26, 2021
1 parent 7aef7c9 commit dd7e246
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { AdministrationSubTab } from '../types';
import { appendSearch } from '../../common/components/link_to/helpers';
import { EndpointIndexUIQueryParams } from '../pages/endpoint_hosts/types';
import { TrustedAppsListPageLocation } from '../pages/trusted_apps/state';
import { EventFiltersPageLocation } from '../pages/event_filters/state';
import { EventFiltersListPageUrlSearchParams } from '../pages/event_filters/types';

// Taken from: https://github.com/microsoft/TypeScript/issues/12936#issuecomment-559034150
Expand Down Expand Up @@ -118,6 +119,26 @@ const normalizeTrustedAppsPageLocation = (
}
};

const normalizeEventFiltersPageLocation = (
location?: Partial<EventFiltersPageLocation>
): Partial<EventFiltersPageLocation> => {
if (location) {
return {
...(!isDefaultOrMissing(location.page_index, MANAGEMENT_DEFAULT_PAGE)
? { page_index: location.page_index }
: {}),
...(!isDefaultOrMissing(location.page_size, MANAGEMENT_DEFAULT_PAGE_SIZE)
? { page_size: location.page_size }
: {}),
...(!isDefaultOrMissing(location.show, undefined) ? { show: location.show } : {}),
...(!isDefaultOrMissing(location.id, undefined) ? { id: location.id } : {}),
...(!isDefaultOrMissing(location.filter, '') ? { filter: location.filter } : ''),
};
} else {
return {};
}
};

/**
* Given an object with url params, and a given key, return back only the first param value (case multiples were defined)
* @param query
Expand Down Expand Up @@ -181,12 +202,27 @@ export const getTrustedAppsListPath = (location?: Partial<TrustedAppsListPageLoc
)}`;
};

export const extractEventFiltetrsPageLocation = (
query: querystring.ParsedUrlQuery
): EventFiltersPageLocation => {
const showParamValue = extractFirstParamValue(query, 'show') as EventFiltersPageLocation['show'];

return {
...extractListPaginationParams(query),
show:
showParamValue && ['edit', 'create'].includes(showParamValue) ? showParamValue : undefined,
id: extractFirstParamValue(query, 'id'),
};
};

export const getEventFiltersListPath = (
location?: Partial<EventFiltersListPageUrlSearchParams>
): string => {
const path = generatePath(MANAGEMENT_ROUTING_EVENT_FILTERS_PATH, {
tabName: AdministrationSubTab.eventFilters,
});

return `${path}${appendSearch(querystring.stringify(location))}`;
return `${path}${appendSearch(
querystring.stringify(normalizeEventFiltersPageLocation(location))
)}`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@

import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports';
import { AsyncResourceState } from '../../../state/async_resource_state';

export interface EventFiltersPageLocation {
page_index: number;
page_size: number;
show?: 'create' | 'edit';
/** Used for editing. The ID of the selected event filter */
id?: string;
filter: string;
}
export interface EventFiltersListPageState {
entries: ExceptionListItemSchema[];
form: {
Expand All @@ -16,4 +25,5 @@ export interface EventFiltersListPageState {
hasOSError: boolean;
submissionResourceState: AsyncResourceState<ExceptionListItemSchema>;
};
location: EventFiltersPageLocation;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ export const initialEventFiltersPageState = (): EventFiltersListPageState => ({
hasOSError: false,
submissionResourceState: { type: 'UninitialisedResourceState' },
},
location: {
page_index: 0,
page_size: 10,
filter: '',
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,21 @@ describe('reducer', () => {
expect(result.entries[0]!.meta).not.toBeUndefined();
});
});
describe('UserChangedUrl', () => {
it('receives a url change with show=create', () => {
const result = eventFiltersPageReducer(initialState, {
type: 'userChangedUrl',
payload: { search: '?show=create', pathname: '/event_filters', hash: '' },
});

expect(result).toStrictEqual({
...initialState,
location: {
...initialState.location,
id: undefined,
show: 'create',
},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@
* 2.0.
*/

// eslint-disable-next-line import/no-nodejs-modules
import { parse } from 'querystring';
import { matchPath } from 'react-router-dom';
import { ImmutableReducer } from '../../../../common/store';
import { Immutable } from '../../../../../common/endpoint/types';
import { AppAction } from '../../../../common/store/actions';
import { AppLocation, Immutable } from '../../../../../common/endpoint/types';
import { UserChangedUrl } from '../../../../common/store/routing/action';
import { MANAGEMENT_ROUTING_EVENT_FILTERS_PATH } from '../../../common/constants';
import { extractEventFiltetrsPageLocation } from '../../../common/routing';

import {
EventFiltersInitForm,
Expand All @@ -25,6 +31,15 @@ type CaseReducer<T extends AppAction> = (
action: Immutable<T>
) => Immutable<EventFiltersListPageState>;

const isEventFIltersPageLocation = (location: Immutable<AppLocation>) => {
return (
matchPath(location.pathname ?? '', {
path: MANAGEMENT_ROUTING_EVENT_FILTERS_PATH,
exact: true,
}) !== null
);
};

const eventFiltersInitForm: CaseReducer<EventFiltersInitForm> = (state, action) => {
return {
...state,
Expand Down Expand Up @@ -77,6 +92,15 @@ const eventFiltersCreateSuccess: CaseReducer<EventFiltersCreateSuccess> = (state
};
};

const userChangedUrl: CaseReducer<UserChangedUrl> = (state, action) => {
if (isEventFIltersPageLocation(action.payload)) {
const location = extractEventFiltetrsPageLocation(parse(action.payload.search.slice(1)));
return { ...state, location };
} else {
return initialEventFiltersPageState();
}
};

export const eventFiltersPageReducer: StateReducer = (
state = initialEventFiltersPageState(),
action
Expand All @@ -90,6 +114,8 @@ export const eventFiltersPageReducer: StateReducer = (
return eventFiltersFormStateChanged(state, action);
case 'eventFiltersCreateSuccess':
return eventFiltersCreateSuccess(state, action);
case 'userChangedUrl':
return userChangedUrl(state, action);
}

return state;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { EventFiltersListPageState } from '../state';
import { EventFiltersListPageState, EventFiltersPageLocation } from '../state';
import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports';
import { ServerApiError } from '../../../../common/types';
import {
Expand Down Expand Up @@ -37,3 +37,6 @@ export const getCreationError = (state: EventFiltersListPageState): ServerApiErr

return isFailedResourceState(submissionResourceState) ? submissionResourceState.error : undefined;
};

export const getCurrentLocation = (state: EventFiltersListPageState): EventFiltersPageLocation =>
state.location;
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
*/

import { initialEventFiltersPageState } from './builders';
import { getFormEntry, getFormHasError } from './selector';
import { getFormEntry, getFormHasError, getCurrentLocation } from './selector';
import { ecsEventMock } from '../test_utils';
import { getInitialExceptionFromEvent } from './utils';
import { EventFiltersPageLocation } from '../state';

const initialState = initialEventFiltersPageState();

Expand Down Expand Up @@ -89,4 +90,19 @@ describe('selectors', () => {
expect(getFormHasError(state)).toBeFalsy();
});
});
describe('getCurrentLocation()', () => {
it('returns current locations', () => {
const expectedLocation: EventFiltersPageLocation = {
show: 'create',
page_index: 1,
page_size: 20,
filter: 'filter',
};
const state = {
...initialState,
location: expectedLocation,
};
expect(getCurrentLocation(state)).toBe(expectedLocation);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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, { memo } from 'react';
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';

export const Empty = memo<{
onAdd: () => void;
/** Should the Add button be disabled */
isAddDisabled?: boolean;
}>(({ onAdd, isAddDisabled = false }) => {
return (
<EuiEmptyPrompt
data-test-subj="eventFiltersEmpty"
iconType="plusInCircle"
title={
<h2>
<FormattedMessage
id="xpack.securitySolution.eventFilters.listEmpty.title"
defaultMessage="Add your first Endpoint Event Filter"
/>
</h2>
}
body={
<FormattedMessage
id="xpack.securitySolution.eventFilters.listEmpty.message"
defaultMessage="There are currently no Endpoint Event Filters on your endpoint."
/>
}
actions={
<EuiButton
fill
isDisabled={isAddDisabled}
onClick={onAdd}
data-test-subj="eventFiltersListAddButton"
>
<FormattedMessage
id="xpack.securitySolution.eventFilters.list.addButton"
defaultMessage="Add Endpoint Event Filter"
/>
</EuiButton>
}
/>
);
});

Empty.displayName = 'Empty';
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,24 @@ import React, { memo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { AdministrationListPage } from '../../../components/administration_list_page';
import { Empty } from './components/empty';
import { useEventFiltersNavigateCallback, useEventFiltersSelector } from './hooks';
import { getCurrentLocation } from '../store/selector';
import { EventFiltersFlyout } from './components/flyout';

export const EventFiltersListPage = memo(() => {
const handleAddButtonClick = useEventFiltersNavigateCallback(() => ({
show: 'create',
id: undefined,
}));

const handleFlyoutClose = useEventFiltersNavigateCallback(() => ({
show: undefined,
id: undefined,
}));

const location = useEventFiltersSelector(getCurrentLocation);
const showFlyout = !!location.show;
return (
<AdministrationListPage
beta={false}
Expand All @@ -25,6 +41,8 @@ export const EventFiltersListPage = memo(() => {
})}
>
{/* <PaginatedContent />*/}
<Empty onAdd={handleAddButtonClick} isAddDisabled={showFlyout} />
{showFlyout ? <EventFiltersFlyout onCancel={handleFlyoutClose} /> : null}
</AdministrationListPage>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@
* 2.0.
*/

import { useState } from 'react';
import { useState, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';

import { isCreationSuccessful, getFormEntry, getCreationError } from '../store/selector';
import {
isCreationSuccessful,
getFormEntry,
getCreationError,
getCurrentLocation,
} from '../store/selector';

import { useToasts } from '../../../../common/lib/kibana';
import { getCreationSuccessMessage, getCreationErrorMessage } from './translations';

import { State } from '../../../../common/store';
import { EventFiltersListPageState } from '../state';
import { EventFiltersListPageState, EventFiltersPageLocation } from '../state';
import { getEventFiltersListPath } from '../../../common/routing';

import {
MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE as EVENT_FILTER_NS,
Expand Down Expand Up @@ -42,3 +49,19 @@ export const useEventFiltersNotification = () => {
toasts.addDanger(getCreationErrorMessage(creationError));
}
};

export type NavigationCallback = (
...args: Parameters<Parameters<typeof useCallback>[0]>
) => Partial<EventFiltersPageLocation>;

export function useEventFiltersNavigateCallback(callback: NavigationCallback) {
const location = useEventFiltersSelector(getCurrentLocation);
const history = useHistory();

return useCallback(
(...args) => history.push(getEventFiltersListPath({ ...location, ...callback(...args) })),
// TODO: needs more investigation, but if callback is in dependencies list memoization will never happen
// eslint-disable-next-line react-hooks/exhaustive-deps
[history, location]
);
}

0 comments on commit dd7e246

Please sign in to comment.