mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Add sorting for exceptions cards (#145070)
## Add sorting for shared exceptions Currently is possible to sort by Name, Created At, Created By https://user-images.githubusercontent.com/7609147/201640150-dc9d53e4-0d34-4da1-8522-9899d35e7359.mov Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Devin W. Hurley <snowmiser111@gmail.com>
This commit is contained in:
parent
463007f9cc
commit
35e02bcf64
8 changed files with 145 additions and 15 deletions
|
@ -44,6 +44,7 @@ export interface UseExceptionListsProps {
|
|||
notifications: NotificationsStart;
|
||||
initialPagination?: Pagination;
|
||||
hideLists?: readonly string[];
|
||||
initialSort?: Sort;
|
||||
}
|
||||
|
||||
export interface UseExceptionListProps {
|
||||
|
@ -56,6 +57,7 @@ export interface UseExceptionListProps {
|
|||
showEndpointListsOnly: boolean;
|
||||
matchFilters: boolean;
|
||||
onSuccess?: (arg: UseExceptionListItemsSuccess) => void;
|
||||
sort?: Sort;
|
||||
}
|
||||
|
||||
export interface FilterExceptionsOptions {
|
||||
|
@ -81,6 +83,10 @@ export interface ApiListExportProps {
|
|||
onSuccess: (blob: Blob) => void;
|
||||
}
|
||||
|
||||
export interface Sort {
|
||||
field: string;
|
||||
order: string;
|
||||
}
|
||||
export interface Pagination {
|
||||
page: Page;
|
||||
perPage: PerPage;
|
||||
|
@ -168,6 +174,7 @@ export interface ApiCallFetchExceptionListsProps {
|
|||
http: HttpStart;
|
||||
namespaceTypes: string;
|
||||
pagination: Partial<Pagination>;
|
||||
sort?: Sort;
|
||||
filters: string;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
|
|
@ -231,14 +231,15 @@ const fetchExceptionLists = async ({
|
|||
namespaceTypes,
|
||||
pagination,
|
||||
signal,
|
||||
sort,
|
||||
}: ApiCallFetchExceptionListsProps): Promise<FoundExceptionListSchema> => {
|
||||
const query = {
|
||||
filter: filters || undefined,
|
||||
namespace_type: namespaceTypes,
|
||||
page: pagination.page ? `${pagination.page}` : '1',
|
||||
per_page: pagination.perPage ? `${pagination.perPage}` : '20',
|
||||
sort_field: 'exception-list.created_at',
|
||||
sort_order: 'desc',
|
||||
sort_field: sort?.field ? sort?.field : 'exception-list.created_at',
|
||||
sort_order: sort?.order ? sort?.order : 'desc',
|
||||
};
|
||||
|
||||
return http.fetch<FoundExceptionListSchema>(`${EXCEPTION_LIST_URL}/_find`, {
|
||||
|
@ -254,6 +255,7 @@ const fetchExceptionListsWithValidation = async ({
|
|||
namespaceTypes,
|
||||
pagination,
|
||||
signal,
|
||||
sort,
|
||||
}: ApiCallFetchExceptionListsProps): Promise<FoundExceptionListSchema> =>
|
||||
flow(
|
||||
() =>
|
||||
|
@ -265,6 +267,7 @@ const fetchExceptionListsWithValidation = async ({
|
|||
namespaceTypes,
|
||||
pagination,
|
||||
signal,
|
||||
sort,
|
||||
}),
|
||||
toError
|
||||
),
|
||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
|||
ExceptionListSchema,
|
||||
UseExceptionListsProps,
|
||||
Pagination,
|
||||
Sort,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { fetchExceptionLists } from '@kbn/securitysolution-list-api';
|
||||
|
||||
|
@ -22,7 +23,9 @@ export type ReturnExceptionLists = [
|
|||
exceptionLists: ExceptionListSchema[],
|
||||
pagination: Pagination,
|
||||
setPagination: React.Dispatch<React.SetStateAction<Pagination>>,
|
||||
fetchLists: Func | null
|
||||
fetchLists: Func | null,
|
||||
sort: Sort,
|
||||
setSort: React.Dispatch<React.SetStateAction<Sort>>
|
||||
];
|
||||
|
||||
const DEFAULT_PAGINATION = {
|
||||
|
@ -31,6 +34,11 @@ const DEFAULT_PAGINATION = {
|
|||
total: 0,
|
||||
};
|
||||
|
||||
const DEFAULT_SORT = {
|
||||
field: 'created_at',
|
||||
order: 'desc',
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for fetching ExceptionLists
|
||||
*
|
||||
|
@ -51,9 +59,11 @@ export const useExceptionLists = ({
|
|||
namespaceTypes,
|
||||
notifications,
|
||||
hideLists = [],
|
||||
initialSort = DEFAULT_SORT,
|
||||
}: UseExceptionListsProps): ReturnExceptionLists => {
|
||||
const [exceptionLists, setExceptionLists] = useState<ExceptionListSchema[]>([]);
|
||||
const [pagination, setPagination] = useState<Pagination>(initialPagination);
|
||||
const [sort, setSort] = useState<Sort>(initialSort);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const abortCtrlRef = useRef<AbortController>();
|
||||
|
||||
|
@ -87,6 +97,7 @@ export const useExceptionLists = ({
|
|||
page: pagination.page,
|
||||
perPage: pagination.perPage,
|
||||
},
|
||||
sort,
|
||||
signal: abortCtrlRef.current.signal,
|
||||
});
|
||||
|
||||
|
@ -115,6 +126,7 @@ export const useExceptionLists = ({
|
|||
notifications.toasts,
|
||||
pagination.page,
|
||||
pagination.perPage,
|
||||
sort,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -125,5 +137,5 @@ export const useExceptionLists = ({
|
|||
};
|
||||
}, [fetchData]);
|
||||
|
||||
return [loading, exceptionLists, pagination, setPagination, fetchData];
|
||||
return [loading, exceptionLists, pagination, setPagination, fetchData, sort, setSort];
|
||||
};
|
||||
|
|
|
@ -62,6 +62,8 @@ describe('useExceptionLists', () => {
|
|||
},
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
{ field: 'created_at', order: 'desc' },
|
||||
expect.any(Function),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -102,6 +104,8 @@ describe('useExceptionLists', () => {
|
|||
},
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
{ field: 'created_at', order: 'desc' },
|
||||
expect.any(Function),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -137,6 +141,7 @@ describe('useExceptionLists', () => {
|
|||
namespaceTypes: 'single,agnostic',
|
||||
pagination: { page: 1, perPage: 20 },
|
||||
signal: new AbortController().signal,
|
||||
sort: { field: 'created_at', order: 'desc' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -175,6 +180,10 @@ describe('useExceptionLists', () => {
|
|||
namespaceTypes: 'single,agnostic',
|
||||
pagination: { page: 1, perPage: 20 },
|
||||
signal: new AbortController().signal,
|
||||
sort: {
|
||||
field: 'created_at',
|
||||
order: 'desc',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -38,8 +38,8 @@ export const deleteExceptionListWithoutRuleReference = () => {
|
|||
};
|
||||
|
||||
export const deleteExceptionListWithRuleReference = () => {
|
||||
cy.get(EXCEPTIONS_OVERFLOW_ACTIONS_BTN).first().click();
|
||||
cy.get(EXCEPTIONS_TABLE_DELETE_BTN).first().click();
|
||||
cy.get(EXCEPTIONS_OVERFLOW_ACTIONS_BTN).last().click();
|
||||
cy.get(EXCEPTIONS_TABLE_DELETE_BTN).last().click();
|
||||
cy.get(EXCEPTIONS_TABLE_MODAL).should('exist');
|
||||
cy.get(EXCEPTIONS_TABLE_MODAL_CONFIRM_BTN).first().click();
|
||||
cy.get(EXCEPTIONS_TABLE_MODAL).should('not.exist');
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import type { Sort } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import styled from 'styled-components';
|
||||
import { EuiContextMenuPanel, EuiContextMenuItem, EuiIcon } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
UtilityBar,
|
||||
UtilityBarAction,
|
||||
|
@ -19,12 +23,19 @@ import * as i18n from '../../translations';
|
|||
interface ExceptionsTableUtilityBarProps {
|
||||
onRefresh?: () => void;
|
||||
totalExceptionLists: number;
|
||||
sort?: Sort;
|
||||
setSort?: (s: Sort) => void;
|
||||
sortFields?: Array<{ field: string; label: string; defaultOrder: 'asc' | 'desc' }>;
|
||||
}
|
||||
|
||||
export const ExceptionsTableUtilityBar: React.FC<ExceptionsTableUtilityBarProps> = ({
|
||||
onRefresh,
|
||||
totalExceptionLists,
|
||||
setSort,
|
||||
sort,
|
||||
sortFields,
|
||||
}) => {
|
||||
const selectedSortField = sortFields?.find((sortField) => sortField.field === sort?.field);
|
||||
return (
|
||||
<UtilityBar border>
|
||||
<UtilityBarSection>
|
||||
|
@ -44,8 +55,66 @@ export const ExceptionsTableUtilityBar: React.FC<ExceptionsTableUtilityBarProps>
|
|||
</UtilityBarAction>
|
||||
</UtilityBarGroup>
|
||||
</UtilityBarSection>
|
||||
<UtilityBarSection>
|
||||
<>
|
||||
<UtilityBarGroup>
|
||||
{sort && (
|
||||
<UtilityBarAction
|
||||
dataTestSubj="sortExceptions"
|
||||
iconSide="right"
|
||||
iconType={sort.order === 'asc' ? 'sortUp' : 'sortDown'}
|
||||
popoverPanelPaddingSize={'s'}
|
||||
popoverContent={() => (
|
||||
<EuiContextMenuPanel
|
||||
size="s"
|
||||
items={sortFields?.map((item) => {
|
||||
const isSelectedSortItem = selectedSortField?.field === item.field;
|
||||
let nextSortOrder = item.defaultOrder;
|
||||
if (isSelectedSortItem) {
|
||||
nextSortOrder = sort.order === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key={item.field}
|
||||
onClick={() =>
|
||||
setSort?.({
|
||||
field: item.field,
|
||||
order: nextSortOrder,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SortMenuItem>
|
||||
{item.label}{' '}
|
||||
{selectedSortField?.field === item.field && (
|
||||
<SortIcon type={sort.order === 'asc' ? 'sortUp' : 'sortDown'} />
|
||||
)}
|
||||
</SortMenuItem>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<SortMenuItem>
|
||||
{i18n.SORT_BY}{' '}
|
||||
{sortFields?.find((sortField) => sortField.field === sort.field)?.label}
|
||||
</SortMenuItem>
|
||||
</UtilityBarAction>
|
||||
)}
|
||||
</UtilityBarGroup>
|
||||
</>
|
||||
</UtilityBarSection>
|
||||
</UtilityBar>
|
||||
);
|
||||
};
|
||||
|
||||
const SortMenuItem = styled('div')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const SortIcon = styled(EuiIcon)`
|
||||
margin-left: 8px;
|
||||
`;
|
||||
|
||||
ExceptionsTableUtilityBar.displayName = 'ExceptionsTableUtilityBar';
|
||||
|
|
|
@ -71,6 +71,14 @@ const exceptionReferenceModalInitialState: ReferenceModalState = {
|
|||
listNamespaceType: 'single',
|
||||
};
|
||||
|
||||
const SORT_FIELDS: Array<{ field: string; label: string; defaultOrder: 'asc' | 'desc' }> = [
|
||||
{
|
||||
field: 'created_at',
|
||||
label: i18n.SORT_BY_CREATE_AT,
|
||||
defaultOrder: 'desc',
|
||||
},
|
||||
];
|
||||
|
||||
export const SharedLists = React.memo(() => {
|
||||
const [{ loading: userInfoLoading, canUserCRUD, canUserREAD }] = useUserData();
|
||||
|
||||
|
@ -89,15 +97,23 @@ export const SharedLists = React.memo(() => {
|
|||
const [filters, setFilters] = useState<ExceptionListFilter | undefined>({
|
||||
types: [ExceptionListTypeEnum.DETECTION, ExceptionListTypeEnum.ENDPOINT],
|
||||
});
|
||||
const [loadingExceptions, exceptions, pagination, setPagination, refreshExceptions] =
|
||||
useExceptionLists({
|
||||
errorMessage: i18n.ERROR_EXCEPTION_LISTS,
|
||||
filterOptions: filters,
|
||||
http,
|
||||
namespaceTypes: ['single', 'agnostic'],
|
||||
notifications,
|
||||
hideLists: ALL_ENDPOINT_ARTIFACT_LIST_IDS,
|
||||
});
|
||||
|
||||
const [
|
||||
loadingExceptions,
|
||||
exceptions,
|
||||
pagination,
|
||||
setPagination,
|
||||
refreshExceptions,
|
||||
sort,
|
||||
setSort,
|
||||
] = useExceptionLists({
|
||||
errorMessage: i18n.ERROR_EXCEPTION_LISTS,
|
||||
filterOptions: filters,
|
||||
http,
|
||||
namespaceTypes: ['single', 'agnostic'],
|
||||
notifications,
|
||||
hideLists: ALL_ENDPOINT_ARTIFACT_LIST_IDS,
|
||||
});
|
||||
const [loadingTableInfo, exceptionListsWithRuleRefs, exceptionsListsRef] = useAllExceptionLists({
|
||||
exceptionLists: exceptions ?? [],
|
||||
});
|
||||
|
@ -470,6 +486,9 @@ export const SharedLists = React.memo(() => {
|
|||
<ExceptionsTableUtilityBar
|
||||
totalExceptionLists={exceptionListsWithRuleRefs.length}
|
||||
onRefresh={handleRefresh}
|
||||
setSort={setSort}
|
||||
sort={sort}
|
||||
sortFields={SORT_FIELDS}
|
||||
/>
|
||||
{exceptionListsWithRuleRefs.length > 0 && canUserCRUD !== null && canUserREAD !== null && (
|
||||
<div data-test-subj="exceptionsTable">
|
||||
|
|
|
@ -347,3 +347,14 @@ export const SUCCESS_TITLE = i18n.translate(
|
|||
defaultMessage: 'created list',
|
||||
}
|
||||
);
|
||||
|
||||
export const SORT_BY = i18n.translate('xpack.securitySolution.exceptions.sortBy', {
|
||||
defaultMessage: 'Sort by:',
|
||||
});
|
||||
|
||||
export const SORT_BY_CREATE_AT = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.sortByCreateAt',
|
||||
{
|
||||
defaultMessage: 'Created At',
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue