Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cases] Cases Table: Configure Available Filters Including Custom Fields #172276

Merged
merged 19 commits into from Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
20ac4a2
[Cases] Status & Severity Filters internally as Multi Select (#169356)
jcger Oct 30, 2023
ac10752
Merge branch 'cases/167651' of github.com:elastic/kibana into cases/1…
jcger Nov 3, 2023
6d22df6
Merge branch 'main' of github.com:jcger/kibana into cases/167651
jcger Nov 6, 2023
b5cb887
Merge branch 'main' of github.com:jcger/kibana into cases/167651
jcger Nov 7, 2023
36d8eba
[Cases] Refactor Cases List Filters (#169371)
jcger Nov 8, 2023
c7d2c84
Merge branch 'main' of github.com:jcger/kibana into cases/167651
jcger Nov 8, 2023
375d638
Merge branch 'cases/167651' of github.com:elastic/kibana into cases/1…
jcger Nov 8, 2023
ac0bdb3
[Cases] Cases List - Refactor Solution Filter (#170851)
jcger Nov 9, 2023
120c0bd
[Cases] Enhance Multi Select Component Interface for Custom Fields (#…
jcger Nov 14, 2023
c166e27
Merge branch 'main' of github.com:jcger/kibana into cases/167651
jcger Nov 21, 2023
8931e00
Merge branch 'main' of github.com:jcger/kibana into cases/167651
jcger Nov 21, 2023
8ab1626
merge utils
jcger Nov 21, 2023
1e68656
Merge branch 'main' of github.com:jcger/kibana into cases/167651
jcger Nov 27, 2023
219cbbd
[Cases] Custom Fields as Cases Filters (#171176)
jcger Nov 30, 2023
b7fb4ef
Merge branch 'main' into cases/167651
jcger Nov 30, 2023
f6308f5
reactivate tests
jcger Nov 30, 2023
0eebd26
fix tests
jcger Nov 30, 2023
a9bc769
dont send custom fields if empty
jcger Dec 1, 2023
58e1188
Merge branch 'main' of github.com:jcger/kibana into cases/167651
jcger Dec 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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