Skip to content

Commit

Permalink
[Cases] Cases Table: Configure Available Filters Including Custom Fie…
Browse files Browse the repository at this point in the history
…lds (#172276)

Meta issue #167651
Fixes: #167651

## Summary
Previous PRs merged into this feature branch:
- #169356
- #169371
- #170851
- #171102
- #171176

## Release notes
Case list filter bar can now be customised. Filters can be removed and
custom fields can be used as filters

## Pending issues
- Table in modal shouldn’t load in local storage saved filter options of
status/severity
- Status & Severity filters in url. Filters must be activated if the
user has them deactivated
- UI overflow when to much filters are active
- Race condition: When a user has a custom field active with an option
selected and this custom field gets removed in settings, it includes the
removed custom field when refreshing. This request will fail, triggering
a second one which won't include the removed custom field
- Found during QA. In the modal, when trying to select all options in
the solutions filter, when checking the last unchecked option, it resets
and there is no checked option anymore

## Flaky test runner link

https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/4128

---------

Co-authored-by: Antonio <antoniodcoelho@gmail.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people committed Dec 4, 2023
1 parent bff1103 commit 90d6358
Show file tree
Hide file tree
Showing 75 changed files with 3,285 additions and 1,250 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/cases/common/constants/index.ts
Expand Up @@ -204,6 +204,7 @@ export const LOCAL_STORAGE_KEYS = {
casesQueryParams: 'cases.list.queryParams',
casesFilterOptions: 'cases.list.filterOptions',
casesTableColumns: 'cases.list.tableColumns',
casesTableFiltersConfig: 'cases.list.tableFiltersConfig',
};

/**
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/cases/common/index.ts
Expand Up @@ -59,7 +59,6 @@ export {
export type { AttachmentAttributes } from './types/domain';
export { ConnectorTypes, AttachmentType, ExternalReferenceStorageType } from './types/domain';
export { getCasesFromAlertsUrl, getCaseFindUserActionsUrl, throwErrors } from './api';
export { StatusAll } from './ui/types';
export { createUICapabilities, type CasesUiCapabilities } from './utils/capabilities';
export { getApiTags, type CasesApiTags } from './utils/api_tags';
export { CaseMetricsFeature } from './types/api';
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/cases/common/types/api/case/v1.ts
Expand Up @@ -189,11 +189,11 @@ export const CasesFindRequestRt = rt.intersection([
/**
* The status of the case (open, closed, in-progress)
*/
status: CaseStatusRt,
status: rt.union([CaseStatusRt, rt.array(CaseStatusRt)]),
/**
* The severity of the case
*/
severity: CaseSeverityRt,
severity: rt.union([CaseSeverityRt, rt.array(CaseSeverityRt)]),
/**
* The uids of the user profiles to filter by
*/
Expand Down
25 changes: 14 additions & 11 deletions x-pack/plugins/cases/common/ui/types.ts
Expand Up @@ -30,6 +30,7 @@ import type {
ExternalReferenceAttachment,
PersistableStateAttachment,
Configuration,
CustomFieldTypes,
} from '../types/domain';
import type {
CasePatchRequest,
Expand Down Expand Up @@ -69,14 +70,6 @@ export interface CasesUiConfigType {
};
}

export const StatusAll = 'all' as const;
export type StatusAllType = typeof StatusAll;

export type CaseStatusWithAllStatus = CaseStatuses | StatusAllType;

export const SeverityAll = 'all' as const;
export type CaseSeverityWithAll = CaseSeverity | typeof SeverityAll;

export const UserActionTypeAll = 'all' as const;
export type CaseUserActionTypeWithAll = UserActionFindRequestTypes | typeof UserActionTypeAll;

Expand Down Expand Up @@ -157,17 +150,27 @@ export interface ParsedUrlQueryParams extends Partial<UrlQueryParams> {

export type LocalStorageQueryParams = Partial<Omit<QueryParams, 'page'>>;

export interface FilterOptions {
export interface SystemFilterOptions {
search: string;
searchFields: string[];
severity: CaseSeverityWithAll;
status: CaseStatusWithAllStatus;
severity: CaseSeverity[];
status: CaseStatuses[];
tags: string[];
assignees: Array<string | null> | null;
reporters: User[];
owner: string[];
category: string[];
}

export interface FilterOptions extends SystemFilterOptions {
customFields: {
[key: string]: {
type: CustomFieldTypes;
options: string[];
};
};
}

export type PartialFilterOptions = Partial<FilterOptions>;

export type SingleCaseMetrics = SingleCaseMetricsResponse;
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/cases/public/common/mock/test_providers.tsx
Expand Up @@ -129,7 +129,7 @@ export interface AppMockRenderer {
render: UiRender;
coreStart: StartServices;
queryClient: QueryClient;
AppWrapper: React.FC<{ children: React.ReactElement }>;
AppWrapper: React.FC<{ children: React.ReactNode }>;
getFilesClient: () => ScopedFilesClient;
}

Expand Down Expand Up @@ -176,7 +176,7 @@ export const createAppMockRenderer = ({

const getFilesClient = mockGetFilesClient();

const AppWrapper: React.FC<{ children: React.ReactElement }> = ({ children }) => (
const AppWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<I18nProvider>
<KibanaContextProvider services={services}>
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
Expand Down
Expand Up @@ -22,7 +22,7 @@ import {
} from '../../common/mock';
import { useGetCasesMockState, connectorsMock } from '../../containers/mock';

import { SortFieldCase, StatusAll } from '../../../common/ui/types';
import { SortFieldCase } from '../../../common/ui/types';
import { CaseSeverity, CaseStatuses } from '../../../common/types/domain';
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
import { getEmptyCellValue } from '../empty_value';
Expand All @@ -38,11 +38,8 @@ import { useGetSupportedActionConnectors } from '../../containers/configure/use_
import { useGetTags } from '../../containers/use_get_tags';
import { useGetCategories } from '../../containers/use_get_categories';
import { useUpdateCase } from '../../containers/use_update_case';
import {
useGetCases,
DEFAULT_QUERY_PARAMS,
DEFAULT_FILTER_OPTIONS,
} from '../../containers/use_get_cases';
import { useGetCases } from '../../containers/use_get_cases';
import { DEFAULT_QUERY_PARAMS, DEFAULT_FILTER_OPTIONS } from '../../containers/constants';
import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get_current_user_profile';
import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock';
import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles';
Expand Down Expand Up @@ -100,7 +97,7 @@ describe('AllCasesListGeneric', () => {
};

const defaultColumnArgs = {
filterStatus: CaseStatuses.open,
filterStatus: [CaseStatuses.open],
handleIsLoading: jest.fn(),
isLoadingCases: [],
isLoadingColumns: false,
Expand Down Expand Up @@ -403,7 +400,8 @@ describe('AllCasesListGeneric', () => {
it('should sort by status', async () => {
appMockRenderer.render(<AllCasesList isSelectorView={false} />);

userEvent.click(screen.getByTitle('Status'));
// 0 is the status filter button label
userEvent.click(screen.getAllByTitle('Status')[1]);

await waitFor(() => {
expect(useGetCasesMock).toHaveBeenLastCalledWith(
Expand All @@ -424,14 +422,16 @@ describe('AllCasesListGeneric', () => {
expect(screen.getByTitle('Name')).toBeInTheDocument();
expect(screen.getByTitle('Category')).toBeInTheDocument();
expect(screen.getByTitle('Created on')).toBeInTheDocument();
expect(screen.getByTitle('Severity')).toBeInTheDocument();
// 0 is the severity filter button label
expect(screen.getAllByTitle('Severity')[1]).toBeInTheDocument();
});
});

it('should sort by severity', async () => {
appMockRenderer.render(<AllCasesList isSelectorView={false} />);

userEvent.click(screen.getByTitle('Severity'));
// 0 is the severity filter button label
userEvent.click(screen.getAllByTitle('Severity')[1]);

await waitFor(() => {
expect(useGetCasesMock).toHaveBeenLastCalledWith(
Expand Down Expand Up @@ -503,15 +503,15 @@ describe('AllCasesListGeneric', () => {
it('should filter by category', async () => {
appMockRenderer.render(<AllCasesList isSelectorView={false} />);

userEvent.click(screen.getByTestId('options-filter-popover-button-Categories'));
userEvent.click(screen.getByTestId('options-filter-popover-button-category'));
await waitForEuiPopoverOpen();
userEvent.click(screen.getByTestId('options-filter-popover-item-twix'));

await waitFor(() => {
expect(useGetCasesMock).toHaveBeenLastCalledWith({
filterOptions: {
...DEFAULT_FILTER_OPTIONS,
searchFields: [],
searchFields: ['title', 'description'],
owner: ['securitySolution'],
category: ['twix'],
},
Expand All @@ -520,72 +520,22 @@ describe('AllCasesListGeneric', () => {
});
});

it('should filter by status: closed', async () => {
appMockRenderer.render(<AllCasesList isSelectorView={false} />);
userEvent.click(screen.getByTestId('case-status-filter'));
await waitForEuiPopoverOpen();
userEvent.click(screen.getByTestId('case-status-filter-closed'));
await waitFor(() => {
expect(useGetCasesMock).toHaveBeenLastCalledWith(
expect.objectContaining({
queryParams: { ...DEFAULT_QUERY_PARAMS, sortField: SortFieldCase.closedAt },
})
);
});
});

it('should filter by status: in-progress', async () => {
appMockRenderer.render(<AllCasesList isSelectorView={false} />);
userEvent.click(screen.getByTestId('case-status-filter'));
await waitForEuiPopoverOpen();
userEvent.click(screen.getByTestId('case-status-filter-in-progress'));
await waitFor(() => {
expect(useGetCasesMock).toHaveBeenLastCalledWith(
expect.objectContaining({
queryParams: DEFAULT_QUERY_PARAMS,
})
);
});
});

it('should filter by status: open', async () => {
appMockRenderer.render(<AllCasesList isSelectorView={false} />);
userEvent.click(screen.getByTestId('case-status-filter'));
await waitForEuiPopoverOpen();
userEvent.click(screen.getByTestId('case-status-filter-in-progress'));
await waitFor(() => {
expect(useGetCasesMock).toHaveBeenLastCalledWith(
expect.objectContaining({
queryParams: DEFAULT_QUERY_PARAMS,
})
);
});
});

it('should show the correct count on stats', async () => {
appMockRenderer.render(<AllCasesList isSelectorView={false} />);

userEvent.click(screen.getByTestId('case-status-filter'));
userEvent.click(screen.getByTestId('options-filter-popover-button-status'));

await waitFor(() => {
expect(screen.getByTestId('case-status-filter-open')).toHaveTextContent('Open (20)');
expect(screen.getByTestId('case-status-filter-in-progress')).toHaveTextContent(
expect(screen.getByTestId('options-filter-popover-item-open')).toHaveTextContent('Open (20)');
expect(screen.getByTestId('options-filter-popover-item-in-progress')).toHaveTextContent(
'In progress (40)'
);
expect(screen.getByTestId('case-status-filter-closed')).toHaveTextContent('Closed (130)');
expect(screen.getByTestId('options-filter-popover-item-closed')).toHaveTextContent(
'Closed (130)'
);
});
});

it('renders the first available status when hiddenStatus is given', async () => {
appMockRenderer.render(
<AllCasesList hiddenStatuses={[StatusAll, CaseStatuses.open]} isSelectorView={true} />
);

await waitFor(() =>
expect(screen.getAllByTestId('case-status-badge-in-progress')[0]).toBeInTheDocument()
);
});

it('shows Solution column if there are no set owners', async () => {
render(
<TestProviders owner={[]}>
Expand Down Expand Up @@ -642,9 +592,9 @@ describe('AllCasesListGeneric', () => {
expect(checkbox).toBeChecked();
}

userEvent.click(screen.getByTestId('case-status-filter'));
userEvent.click(screen.getByTestId('options-filter-popover-button-status'));
await waitForEuiPopoverOpen();
userEvent.click(screen.getByTestId('case-status-filter-closed'));
userEvent.click(screen.getByTestId('options-filter-popover-item-open'));

for (const checkbox of checkboxes) {
expect(checkbox).not.toBeChecked();
Expand Down Expand Up @@ -701,24 +651,25 @@ describe('AllCasesListGeneric', () => {
expect(useGetCasesMock).toHaveBeenCalledWith({
filterOptions: {
search: '',
searchFields: [],
severity: 'all',
searchFields: ['title', 'description'],
severity: [],
reporters: [],
status: 'all',
status: [],
tags: [],
assignees: [],
owner: ['securitySolution', 'observability'],
category: [],
customFields: {},
},
queryParams: DEFAULT_QUERY_PARAMS,
});

userEvent.click(getByTestId('solution-filter-popover-button'));
userEvent.click(getByTestId('options-filter-popover-button-owner'));

await waitForEuiPopoverOpen();

userEvent.click(
getByTestId(`solution-filter-popover-item-${SECURITY_SOLUTION_OWNER}`),
getByTestId(`options-filter-popover-item-${SECURITY_SOLUTION_OWNER}`),
undefined,
{
skipPointerEventsCheck: true,
Expand All @@ -728,20 +679,21 @@ describe('AllCasesListGeneric', () => {
expect(useGetCasesMock).toBeCalledWith({
filterOptions: {
search: '',
searchFields: [],
severity: 'all',
searchFields: ['title', 'description'],
severity: [],
reporters: [],
status: 'all',
status: [],
tags: [],
assignees: [],
owner: ['securitySolution'],
category: [],
customFields: {},
},
queryParams: DEFAULT_QUERY_PARAMS,
});

userEvent.click(
getByTestId(`solution-filter-popover-item-${SECURITY_SOLUTION_OWNER}`),
getByTestId(`options-filter-popover-item-${SECURITY_SOLUTION_OWNER}`),
undefined,
{
skipPointerEventsCheck: true,
Expand All @@ -751,14 +703,15 @@ describe('AllCasesListGeneric', () => {
expect(useGetCasesMock).toHaveBeenLastCalledWith({
filterOptions: {
search: '',
searchFields: [],
severity: 'all',
searchFields: ['title', 'description'],
severity: [],
reporters: [],
status: 'all',
status: [],
tags: [],
assignees: [],
owner: ['securitySolution', 'observability'],
category: [],
customFields: {},
},
queryParams: DEFAULT_QUERY_PARAMS,
});
Expand All @@ -771,7 +724,7 @@ describe('AllCasesListGeneric', () => {
</TestProviders>
);

expect(queryByTestId('solution-filter-popover-button')).toBeFalsy();
expect(queryByTestId('options-filter-popover-button-owner')).toBeFalsy();
});

it('should call useGetCases with the correct owner on initial render', async () => {
Expand All @@ -784,14 +737,15 @@ describe('AllCasesListGeneric', () => {
expect(useGetCasesMock).toHaveBeenCalledWith({
filterOptions: {
search: '',
searchFields: [],
severity: 'all',
searchFields: ['title', 'description'],
severity: [],
reporters: [],
status: 'all',
status: [],
tags: [],
assignees: [],
owner: ['securitySolution'],
category: [],
customFields: {},
},
queryParams: DEFAULT_QUERY_PARAMS,
});
Expand Down

0 comments on commit 90d6358

Please sign in to comment.