From a04a03b438769ad38937f67a6cd8c155de2f31de Mon Sep 17 00:00:00 2001 From: Antonio Date: Wed, 25 Jan 2023 12:27:11 +0100 Subject: [PATCH] [Cases] Enable case search by ID (#149233) Fixes #148084 [The uuid PR was merged](https://github.com/elastic/kibana/pull/149135) so I am removing the `draft` status here. ## Summary This PR introduces search by UUID in the Cases table. If a user puts a UUID in the search bar and presses enter the search result will now return the case with that ID. Additionally, we look for the matches of that search text in the title and description fields. See the example below: Screenshot 2023-01-19 at 16 06 53 We are searching for `733e1c40-9586-11ed-a29f-8b57be9cf211`. There are two matches because that search text matches the ID of a case and the title of another. ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) ### Release notes Users can now search for Cases by ID. Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/cases/common/api/cases/case.ts | 4 ++ .../components/all_cases/translations.ts | 2 +- .../cases/server/authorization/mock.ts | 4 +- .../cases/server/client/cases/find.test.ts | 66 +++++++++++++++++++ .../plugins/cases/server/client/cases/find.ts | 7 +- x-pack/plugins/cases/server/client/factory.ts | 1 + .../client/metrics/get_case_metrics.test.ts | 3 - .../client/metrics/test_utils/client.ts | 3 - x-pack/plugins/cases/server/client/mocks.ts | 61 +++++++++++++---- x-pack/plugins/cases/server/client/types.ts | 2 + .../plugins/cases/server/client/utils.test.ts | 46 +++++++++++-- x-pack/plugins/cases/server/client/utils.ts | 47 ++++++++++--- x-pack/plugins/cases/tsconfig.json | 1 + .../tests/common/cases/find_cases.ts | 44 +++++++++++++ x-pack/test/functional/services/cases/list.ts | 17 ++++- .../apps/cases/list_view.ts | 57 +++++++++++++--- 16 files changed, 323 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/cases/server/client/cases/find.test.ts diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 6bfcf1869d850..1463f3cb12d50 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -213,6 +213,10 @@ export const CasesFindRequestRt = rt.partial({ * The fields to perform the simple_query_string parsed query against */ searchFields: rt.union([rt.array(rt.string), rt.string]), + /** + * The root fields to perform the simple_query_string parsed query against + */ + rootSearchFields: rt.array(rt.string), /** * The field to use for sorting the found objects. * diff --git a/x-pack/plugins/cases/public/components/all_cases/translations.ts b/x-pack/plugins/cases/public/components/all_cases/translations.ts index 2c5f269754178..b63d14768f770 100644 --- a/x-pack/plugins/cases/public/components/all_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/all_cases/translations.ts @@ -64,7 +64,7 @@ export const INCIDENT_MANAGEMENT_SYSTEM = i18n.translate('xpack.cases.caseTable. }); export const SEARCH_PLACEHOLDER = i18n.translate('xpack.cases.caseTable.searchPlaceholder', { - defaultMessage: 'e.g. case name', + defaultMessage: 'Search cases', }); export const CLOSED = i18n.translate('xpack.cases.caseTable.closed', { diff --git a/x-pack/plugins/cases/server/authorization/mock.ts b/x-pack/plugins/cases/server/authorization/mock.ts index d72feedd30bf2..757fa8d7ec9df 100644 --- a/x-pack/plugins/cases/server/authorization/mock.ts +++ b/x-pack/plugins/cases/server/authorization/mock.ts @@ -14,7 +14,9 @@ export type AuthorizationMock = jest.Mocked; export const createAuthorizationMock = () => { const mocked: AuthorizationMock = { ensureAuthorized: jest.fn(), - getAuthorizationFilter: jest.fn(), + getAuthorizationFilter: jest.fn().mockImplementation(async () => { + return { filter: undefined, ensureSavedObjectsAreAuthorized: () => {} }; + }), getAndEnsureAuthorizedEntities: jest.fn(), }; return mocked; diff --git a/x-pack/plugins/cases/server/client/cases/find.test.ts b/x-pack/plugins/cases/server/client/cases/find.test.ts new file mode 100644 index 0000000000000..f23ad82df1fcf --- /dev/null +++ b/x-pack/plugins/cases/server/client/cases/find.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { v1 as uuidv1 } from 'uuid'; + +import type { CaseResponse } from '../../../common/api'; + +import { flattenCaseSavedObject } from '../../common/utils'; +import { mockCases } from '../../mocks'; +import { createCasesClientMockArgs, createCasesClientMockFindRequest } from '../mocks'; +import { find } from './find'; + +describe('find', () => { + describe('constructSearch', () => { + const clientArgs = createCasesClientMockArgs(); + const casesMap = new Map( + mockCases.map((obj) => { + return [obj.id, flattenCaseSavedObject({ savedObject: obj, totalComment: 2 })]; + }) + ); + clientArgs.services.caseService.findCasesGroupedByID.mockResolvedValue({ + page: 1, + perPage: 10, + total: casesMap.size, + casesMap, + }); + clientArgs.services.caseService.getCaseStatusStats.mockResolvedValue({ + open: 1, + 'in-progress': 2, + closed: 3, + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('search by uuid updates search term and adds rootSearchFields', async () => { + const search = uuidv1(); + const findRequest = createCasesClientMockFindRequest({ search }); + + await find(findRequest, clientArgs); + await expect(clientArgs.services.caseService.findCasesGroupedByID).toHaveBeenCalled(); + + const call = clientArgs.services.caseService.findCasesGroupedByID.mock.calls[0][0]; + + expect(call.caseOptions.search).toBe(`"${search}" "cases:${search}"`); + expect(call.caseOptions).toHaveProperty('rootSearchFields'); + expect(call.caseOptions.rootSearchFields).toStrictEqual(['_id']); + }); + + it('regular search term does not cause rootSearchFields to be appended', async () => { + const search = 'foobar'; + const findRequest = createCasesClientMockFindRequest({ search }); + await find(findRequest, clientArgs); + await expect(clientArgs.services.caseService.findCasesGroupedByID).toHaveBeenCalled(); + + const call = clientArgs.services.caseService.findCasesGroupedByID.mock.calls[0][0]; + + expect(call.caseOptions.search).toBe(search); + expect(call.caseOptions).not.toHaveProperty('rootSearchFields'); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index cb64123c99de9..bcbd9301f7e5e 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -16,7 +16,7 @@ import { CasesFindRequestRt, throwErrors, CasesFindResponseRt, excess } from '.. import { createCaseError } from '../../common/error'; import { asArray, transformCases } from '../../common/utils'; -import { constructQueryOptions } from '../utils'; +import { constructQueryOptions, constructSearch } from '../utils'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; import { Operations } from '../../authorization'; import type { CasesClientArgs } from '..'; @@ -36,6 +36,8 @@ export const find = async ( services: { caseService, licensingService }, authorization, logger, + savedObjectsSerializer, + spaceId, } = clientArgs; try { @@ -85,11 +87,14 @@ export const find = async ( const caseQueryOptions = constructQueryOptions({ ...queryArgs, authorizationFilter }); + const caseSearch = constructSearch(queryParams.search, spaceId, savedObjectsSerializer); + const [cases, statusStats] = await Promise.all([ caseService.findCasesGroupedByID({ caseOptions: { ...queryParams, ...caseQueryOptions, + ...caseSearch, searchFields: asArray(queryParams.searchFields), fields: includeFieldsRequiredForAuthentication(fields), }, diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index 63f0fdcb5a12b..57f54a745cf2d 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -145,6 +145,7 @@ export class CasesClientFactory { securityStartPlugin: this.options.securityPluginStart, publicBaseUrl: this.options.publicBaseUrl, spaceId: this.options.spacesPluginStart.spacesService.getSpaceId(request), + savedObjectsSerializer, }); } diff --git a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts index 7f980204af2a2..1fabee2893e06 100644 --- a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts +++ b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts @@ -181,9 +181,6 @@ function createMockClientArgs() { }); const authorization = createAuthorizationMock(); - authorization.getAuthorizationFilter.mockImplementation(async () => { - return { filter: undefined, ensureSavedObjectsAreAuthorized: () => {} }; - }); const soClient = savedObjectsClientMock.create(); diff --git a/x-pack/plugins/cases/server/client/metrics/test_utils/client.ts b/x-pack/plugins/cases/server/client/metrics/test_utils/client.ts index df66f92cd872a..3b9edbc443167 100644 --- a/x-pack/plugins/cases/server/client/metrics/test_utils/client.ts +++ b/x-pack/plugins/cases/server/client/metrics/test_utils/client.ts @@ -19,9 +19,6 @@ export function createMockClient() { export function createMockClientArgs() { const authorization = createAuthorizationMock(); - authorization.getAuthorizationFilter.mockImplementation(async () => { - return { filter: undefined, ensureSavedObjectsAreAuthorized: () => {} }; - }); const soClient = savedObjectsClientMock.create(); diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 669ca6882fea5..0c2c57253ab9f 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -7,11 +7,29 @@ import type { PublicContract, PublicMethodsOf } from '@kbn/utility-types'; import { loggingSystemMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; -import { securityMock } from '@kbn/security-plugin/server/mocks'; +import type { ISavedObjectsSerializer } from '@kbn/core-saved-objects-server'; +import { securityMock } from '@kbn/security-plugin/server/mocks'; import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client.mock'; import { makeLensEmbeddableFactory } from '@kbn/lens-plugin/server/embeddable/make_lens_embeddable_factory'; +import { serializerMock } from '@kbn/core-saved-objects-base-server-mocks'; + +import type { CasesFindRequest } from '../../common/api'; import type { CasesClient } from '.'; +import type { AttachmentsSubClient } from './attachments/client'; +import type { CasesSubClient } from './cases/client'; +import type { ConfigureSubClient } from './configure/client'; +import type { CasesClientFactory } from './factory'; +import type { MetricsSubClient } from './metrics/client'; +import type { UserActionsSubClient } from './user_actions/client'; + +import { CaseStatuses } from '../../common'; +import { CaseSeverity } from '../../common/api'; +import { SortFieldCase } from '../../public/containers/types'; +import { + createExternalReferenceAttachmentTypeRegistryMock, + createPersistableStateAttachmentTypeRegistryMock, +} from '../attachment_framework/mocks'; import { createAuthorizationMock } from '../authorization/mock'; import { connectorMappingsServiceMock, @@ -23,16 +41,6 @@ import { createUserActionServiceMock, createNotificationServiceMock, } from '../services/mocks'; -import type { AttachmentsSubClient } from './attachments/client'; -import type { CasesSubClient } from './cases/client'; -import type { ConfigureSubClient } from './configure/client'; -import type { CasesClientFactory } from './factory'; -import type { MetricsSubClient } from './metrics/client'; -import type { UserActionsSubClient } from './user_actions/client'; -import { - createExternalReferenceAttachmentTypeRegistryMock, - createPersistableStateAttachmentTypeRegistryMock, -} from '../attachment_framework/mocks'; type CasesSubClientMock = jest.Mocked; @@ -127,6 +135,20 @@ export const createCasesClientFactory = (): CasesClientFactoryMock => { return factory as unknown as CasesClientFactoryMock; }; +type SavedObjectsSerializerMock = jest.Mocked; + +export const createSavedObjectsSerializerMock = (): SavedObjectsSerializerMock => { + const serializer = serializerMock.create(); + serializer.generateRawId.mockImplementation( + (namespace: string | undefined, type: string, id: string) => { + const namespacePrefix = namespace ? `${namespace}:` : ''; + return `${namespacePrefix}${type}:${id}`; + } + ); + + return serializer; +}; + export const createCasesClientMockArgs = () => { return { services: { @@ -160,5 +182,22 @@ export const createCasesClientMockArgs = () => { {} ) ), + savedObjectsSerializer: createSavedObjectsSerializerMock(), }; }; + +export const createCasesClientMockFindRequest = ( + overwrites?: CasesFindRequest +): CasesFindRequest => ({ + search: '', + searchFields: ['title', 'description'], + severity: CaseSeverity.LOW, + assignees: [], + reporters: [], + status: CaseStatuses.open, + tags: [], + owner: [], + sortField: SortFieldCase.createdAt, + sortOrder: 'desc', + ...overwrites, +}); diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 7fded9a5d1a45..1ceca25fb2871 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -11,6 +11,7 @@ import type { ActionsClient } from '@kbn/actions-plugin/server'; import type { LensServerPluginSetup } from '@kbn/lens-plugin/server'; import type { SecurityPluginStart } from '@kbn/security-plugin/server'; import type { IBasePath } from '@kbn/core-http-browser'; +import type { ISavedObjectsSerializer } from '@kbn/core-saved-objects-server'; import type { KueryNode } from '@kbn/es-query'; import type { CasesFindRequest, User } from '../../common/api'; import type { Authorization } from '../authorization/authorization'; @@ -53,6 +54,7 @@ export interface CasesClientArgs { readonly externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry; readonly securityStartPlugin: SecurityPluginStart; readonly spaceId: string; + readonly savedObjectsSerializer: ISavedObjectsSerializer; readonly publicBaseUrl?: IBasePath['publicBaseUrl']; } diff --git a/x-pack/plugins/cases/server/client/utils.test.ts b/x-pack/plugins/cases/server/client/utils.test.ts index 12cbc1bb60358..fb3e89b598b03 100644 --- a/x-pack/plugins/cases/server/client/utils.test.ts +++ b/x-pack/plugins/cases/server/client/utils.test.ts @@ -5,17 +5,23 @@ * 2.0. */ +import { v1 as uuidv1 } from 'uuid'; + +import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; +import { toElasticsearchQuery } from '@kbn/es-query'; + +import { CaseStatuses } from '../../common'; +import { CaseSeverity } from '../../common/api'; +import { ESCaseSeverity, ESCaseStatus } from '../services/cases/types'; +import { createSavedObjectsSerializerMock } from './mocks'; import { arraysDifference, buildNestedFilter, buildRangeFilter, constructQueryOptions, + constructSearch, convertSortField, } from './utils'; -import { toElasticsearchQuery } from '@kbn/es-query'; -import { CaseStatuses } from '../../common'; -import { CaseSeverity } from '../../common/api'; -import { ESCaseSeverity, ESCaseStatus } from '../services/cases/types'; describe('utils', () => { describe('convertSortField', () => { @@ -916,4 +922,36 @@ describe('utils', () => { }); }); }); + + describe('constructSearchById', () => { + const savedObjectsSerializer = createSavedObjectsSerializerMock(); + + it('returns the rootSearchFields and search with correct values when given a uuid', () => { + const uuid = uuidv1(); // the specific version is irrelevant + + expect(constructSearch(uuid, DEFAULT_NAMESPACE_STRING, savedObjectsSerializer)) + .toMatchInlineSnapshot(` + Object { + "rootSearchFields": Array [ + "_id", + ], + "search": "\\"${uuid}\\" \\"cases:${uuid}\\"", + } + `); + }); + + it('search value not changed and no rootSearchFields when search is non-uuid', () => { + const search = 'foobar'; + const result = constructSearch(search, DEFAULT_NAMESPACE_STRING, savedObjectsSerializer); + + expect(result).not.toHaveProperty('rootSearchFields'); + expect(result).toEqual({ search }); + }); + + it('returns undefined if search term undefined', () => { + expect(constructSearch(undefined, DEFAULT_NAMESPACE_STRING, savedObjectsSerializer)).toEqual( + undefined + ); + }); + }); }); diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index cbb373fd32ab9..80924c0df89de 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -11,22 +11,24 @@ import deepEqual from 'fast-deep-equal'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; +import { validate as uuidValidate } from 'uuid'; +import type { ISavedObjectsSerializer } from '@kbn/core-saved-objects-server'; import type { KueryNode } from '@kbn/es-query'; + import { nodeBuilder, fromKueryExpression, escapeKuery } from '@kbn/es-query'; -import { - isCommentRequestTypeExternalReference, - isCommentRequestTypePersistableState, -} from '../../common/utils/attachments'; -import { CASE_SAVED_OBJECT, NO_ASSIGNEES_FILTERING_KEYWORD } from '../../common/constants'; +import { spaceIdToNamespace } from '@kbn/spaces-plugin/server/lib/utils/namespace'; -import { SEVERITY_EXTERNAL_TO_ESMODEL, STATUS_EXTERNAL_TO_ESMODEL } from '../common/constants'; import type { CaseStatuses, CommentRequest, CaseSeverity, CommentRequestExternalReferenceType, + CasesFindRequest, } from '../../common/api'; +import type { SavedObjectFindOptionsKueryNode } from '../common/types'; +import type { CasesFindQueryParams } from './types'; + import { OWNER_FIELD, AlertCommentRequestRt, @@ -39,7 +41,13 @@ import { ExternalReferenceNoSORt, PersistableStateAttachmentRt, } from '../../common/api'; +import { CASE_SAVED_OBJECT, NO_ASSIGNEES_FILTERING_KEYWORD } from '../../common/constants'; +import { + isCommentRequestTypeExternalReference, + isCommentRequestTypePersistableState, +} from '../../common/utils/attachments'; import { combineFilterWithAuthorizationFilter } from '../authorization/utils'; +import { SEVERITY_EXTERNAL_TO_ESMODEL, STATUS_EXTERNAL_TO_ESMODEL } from '../common/constants'; import { getIDsAndIndicesAsArrays, isCommentRequestTypeAlert, @@ -47,8 +55,6 @@ import { isCommentRequestTypeActions, assertUnreachable, } from '../common/utils'; -import type { SavedObjectFindOptionsKueryNode } from '../common/types'; -import type { CasesFindQueryParams } from './types'; export const decodeCommentRequest = (comment: CommentRequest) => { if (isCommentRequestTypeUser(comment)) { @@ -537,3 +543,28 @@ export const convertSortField = (sortField: string | undefined): SortFieldCase = return SortFieldCase.createdAt; } }; + +export const constructSearch = ( + search: string | undefined, + spaceId: string, + savedObjectsSerializer: ISavedObjectsSerializer +): Pick | undefined => { + if (!search) { + return undefined; + } + + if (uuidValidate(search)) { + const rawId = savedObjectsSerializer.generateRawId( + spaceIdToNamespace(spaceId), + CASE_SAVED_OBJECT, + search + ); + + return { + search: `"${search}" "${rawId}"`, + rootSearchFields: ['_id'], + }; + } + + return { search }; +}; diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index af2f1a29a64d1..599545caf0ae5 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -54,6 +54,7 @@ "@kbn/ecs", "@kbn/core-saved-objects-api-server", "@kbn/core-saved-objects-base-server-mocks", + "@kbn/core-saved-objects-utils-server", ], "exclude": [ "target/**/*", diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts index 6c5744ccb0515..c30cac8416bf4 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { v1 as uuidv1 } from 'uuid'; + import expect from '@kbn/expect'; import { CASES_URL } from '@kbn/cases-plugin/common/constants'; import { @@ -345,6 +347,10 @@ export default ({ getService }: FtrProviderContext): void => { await createCase(supertest, postCaseReq); }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + it('should successfully find a case when using valid searchFields', async () => { const cases = await findCases({ supertest, @@ -363,6 +369,44 @@ export default ({ getService }: FtrProviderContext): void => { expect(cases.total).to.be(1); }); + it('should successfully find a case when using a valid uuid', async () => { + const caseWithId = await createCase(supertest, postCaseReq); + + const cases = await findCases({ + supertest, + query: { searchFields: ['title', 'description'], search: caseWithId.id }, + }); + + expect(cases.total).to.be(1); + expect(cases.cases[0].id).to.equal(caseWithId.id); + }); + + it('should successfully find a case with a valid uuid in title', async () => { + const uuid = uuidv1(); + await createCase(supertest, { ...postCaseReq, title: uuid }); + + const cases = await findCases({ + supertest, + query: { searchFields: ['title', 'description'], search: uuid }, + }); + + expect(cases.total).to.be(1); + expect(cases.cases[0].title).to.equal(uuid); + }); + + it('should successfully find a case with a valid uuid in title', async () => { + const uuid = uuidv1(); + await createCase(supertest, { ...postCaseReq, description: uuid }); + + const cases = await findCases({ + supertest, + query: { searchFields: ['title', 'description'], search: uuid }, + }); + + expect(cases.total).to.be(1); + expect(cases.cases[0].description).to.equal(uuid); + }); + it('should not find any cases when it does not use a wildcard and the string does not match', async () => { const cases = await findCases({ supertest, diff --git a/x-pack/test/functional/services/cases/list.ts b/x-pack/test/functional/services/cases/list.ts index c214739d0d263..39713897afaba 100644 --- a/x-pack/test/functional/services/cases/list.ts +++ b/x-pack/test/functional/services/cases/list.ts @@ -115,7 +115,20 @@ export function CasesTableServiceProvider( await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 }); }, - async getCaseFromTable(index: number) { + async getCaseById(caseId: string) { + const targetCase = await find.allByCssSelector( + `[data-test-subj*="cases-table-row-${caseId}"`, + 100 + ); + + if (!targetCase.length) { + throw new Error(`Cannot find case with id ${caseId} on table.`); + } + + return targetCase[0]; + }, + + async getCaseByIndex(index: number) { const rows = await find.allByCssSelector('[data-test-subj*="cases-table-row-"', 100); assertCaseExists(index, rows.length); @@ -361,7 +374,7 @@ export function CasesTableServiceProvider( async getCaseTitle(index: number) { const titleElement = await ( - await this.getCaseFromTable(index) + await this.getCaseByIndex(index) ).findByTestSubject('case-details-link'); return await titleElement.getVisibleText(); diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts b/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts index a137a68831221..a05c11940db7d 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts @@ -281,6 +281,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { describe('filtering', () => { const caseTitle = 'matchme'; + const caseIds: string[] = []; before(async () => { await createUsersAndRoles(getService, users, roles); @@ -288,14 +289,26 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const profiles = await cases.api.suggestUserProfiles({ name: 'all', owners: ['cases'] }); - await cases.api.createCase({ + const case1 = await cases.api.createCase({ title: caseTitle, tags: ['one'], description: 'lots of information about an incident', }); - await cases.api.createCase({ title: 'test2', tags: ['two'] }); - await cases.api.createCase({ title: 'test3', assignees: [{ uid: profiles[0].uid }] }); - await cases.api.createCase({ title: 'test4', assignees: [{ uid: profiles[1].uid }] }); + const case2 = await cases.api.createCase({ title: 'test2', tags: ['two'] }); + const case3 = await cases.api.createCase({ + title: case2.id, + assignees: [{ uid: profiles[0].uid }], + }); + const case4 = await cases.api.createCase({ + title: 'test4', + assignees: [{ uid: profiles[1].uid }], + description: case2.id, + }); + + caseIds.push(case1.id); + caseIds.push(case2.id); + caseIds.push(case3.id); + caseIds.push(case4.id); await header.waitUntilLoadingHasFinished(); await cases.casesTable.waitForCasesToBeListed(); @@ -329,6 +342,34 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.casesTable.validateCasesTableHasNthRows(4); }); + it('filters cases from the list using an id search', async () => { + await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 }); + + const input = await testSubjects.find('search-cases'); + await input.type(caseIds[0]); + await input.pressKeys(browser.keys.ENTER); + + await cases.casesTable.validateCasesTableHasNthRows(1); + await cases.casesTable.getCaseById(caseIds[0]); + await testSubjects.click('clearSearchButton'); + await cases.casesTable.validateCasesTableHasNthRows(4); + }); + + it('id search also matches title and description', async () => { + await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 }); + + const input = await testSubjects.find('search-cases'); + await input.type(caseIds[1]); + await input.pressKeys(browser.keys.ENTER); + + await cases.casesTable.validateCasesTableHasNthRows(3); + await cases.casesTable.getCaseById(caseIds[1]); // id match + await cases.casesTable.getCaseById(caseIds[2]); // title match + await cases.casesTable.getCaseById(caseIds[3]); // description match + await testSubjects.click('clearSearchButton'); + await cases.casesTable.validateCasesTableHasNthRows(4); + }); + it('only shows cases with a wildcard query "test*" matching the title', async () => { await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 }); @@ -336,7 +377,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await input.type('test*'); await input.pressKeys(browser.keys.ENTER); - await cases.casesTable.validateCasesTableHasNthRows(3); + await cases.casesTable.validateCasesTableHasNthRows(2); await testSubjects.click('clearSearchButton'); await cases.casesTable.validateCasesTableHasNthRows(4); }); @@ -381,7 +422,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.casesTable.filterByTag('one'); await cases.casesTable.refreshTable(); await cases.casesTable.validateCasesTableHasNthRows(1); - const row = await cases.casesTable.getCaseFromTable(0); + const row = await cases.casesTable.getCaseByIndex(0); const tags = await row.findByTestSubject('case-table-column-tags-one'); expect(await tags.getVisibleText()).to.be('one'); }); @@ -426,11 +467,11 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await cases.casesTable.validateCasesTableHasNthRows(2); const firstCaseTitle = await ( - await cases.casesTable.getCaseFromTable(0) + await cases.casesTable.getCaseByIndex(0) ).findByTestSubject('case-details-link'); const secondCaseTitle = await ( - await cases.casesTable.getCaseFromTable(1) + await cases.casesTable.getCaseByIndex(1) ).findByTestSubject('case-details-link'); expect(await firstCaseTitle.getVisibleText()).be('test2');