diff --git a/packages/go/chow/ingestvalidator/schema.go b/packages/go/chow/ingestvalidator/schema.go index f47d6d7665c..bfa57099687 100644 --- a/packages/go/chow/ingestvalidator/schema.go +++ b/packages/go/chow/ingestvalidator/schema.go @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/packages/go/openapi/doc/openapi.json b/packages/go/openapi/doc/openapi.json index 6650236dbb3..606342f558a 100644 --- a/packages/go/openapi/doc/openapi.json +++ b/packages/go/openapi/doc/openapi.json @@ -22337,7 +22337,8 @@ "Groups", "Data Quality", "Datapipe", - "Cypher" + "Cypher", + "OpenGraph" ] }, { diff --git a/packages/javascript/bh-shared-ui/src/components/index.ts b/packages/javascript/bh-shared-ui/src/components/index.ts index c5d39e9c5d2..d82c283a010 100644 --- a/packages/javascript/bh-shared-ui/src/components/index.ts +++ b/packages/javascript/bh-shared-ui/src/components/index.ts @@ -33,9 +33,9 @@ export { default as CollectorCard } from './CollectorCard'; export * from './CollectorCardList'; export { default as CollectorCardList } from './CollectorCardList'; export * from './ColumnHeaders'; -export * from './ConditionalTooltip'; export * from './CommunityIcon'; export { default as CommunityIcon } from './CommunityIcon'; +export * from './ConditionalTooltip'; export * from './ConfirmationDialog'; export { default as ConfirmationDialog } from './ConfirmationDialog'; export * from './CreateMenu'; diff --git a/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/RuleForm/RuleForm.test.tsx b/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/RuleForm/RuleForm.test.tsx index 9228d09fd82..68ac85c49c7 100644 --- a/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/RuleForm/RuleForm.test.tsx +++ b/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/RuleForm/RuleForm.test.tsx @@ -439,4 +439,38 @@ describe('Rule Form', () => { '/ui/explore?searchType=cypher&exploreSearchTab=cypher&cypherSearch=aGVsbG8%2Bd29ybGQ%3D' ); }); + + it('calls handleError with ruleType when creating a rule fails', async () => { + vi.mocked(useParams).mockReturnValue({ zoneId: '1', ruleId: undefined }); + + server.use( + rest.post('/api/v2/asset-group-tags/:tagId/selectors', (_, res, ctx) => { + return res( + ctx.status(400), + ctx.json({ + errors: [{ message: 'seeds are required' }], + }) + ); + }) + ); + + render(); + + const nameInput = await screen.findByLabelText('Name'); + await user.click(nameInput); + await user.paste('test rule'); + + // Submit without adding any seeds — should trigger the "seeds are required" path + await user.click(await screen.findByRole('button', { name: /Create Rule/ })); + + await waitFor(() => { + expect(handleErrorSpy).toHaveBeenCalledWith( + expect.anything(), + 'creating', + 'rule', + expect.any(Function), + expect.objectContaining({ ruleType: expect.any(Number) }) + ); + }); + }); }); diff --git a/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/RuleForm/RuleForm.tsx b/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/RuleForm/RuleForm.tsx index fa7a429e4a9..a936de50f2e 100644 --- a/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/RuleForm/RuleForm.tsx +++ b/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/RuleForm/RuleForm.tsx @@ -163,7 +163,7 @@ const RuleForm: FC = () => { // In the API, PATCHing with an empty seeds array ignore the array. if (Array.isArray(diffedValues.seeds) && diffedValues.seeds.length === 0) { return addNotification( - getErrorMessage('seeds are required', 'updating', 'rule'), + getErrorMessage('seeds are required', 'updating', 'rule', ruleType), 'privilege-zones_updating-rule', { anchorOrigin: { vertical: 'top', horizontal: 'right' }, @@ -190,9 +190,9 @@ const RuleForm: FC = () => { navigate(-1); } catch (error) { - handleError(error, 'updating', 'rule', addNotification); + handleError(error, 'updating', 'rule', addNotification, { ruleType }); } - }, [tagId, ruleId, patchRuleMutation, addNotification, navigate, ruleQuery.data, form, seeds]); + }, [tagId, ruleId, ruleType, patchRuleMutation, addNotification, navigate, ruleQuery.data, form, seeds]); const handleCreateRule = useCallback(async () => { try { @@ -212,9 +212,9 @@ const RuleForm: FC = () => { navigate(tagDetailsLink(tagId)); } catch (error) { - handleError(error, 'creating', 'rule', addNotification); + handleError(error, 'creating', 'rule', addNotification, { ruleType }); } - }, [tagId, form, seeds, createRuleMutation, addNotification, navigate, tagDetailsLink]); + }, [tagId, ruleType, form, seeds, createRuleMutation, addNotification, navigate, tagDetailsLink]); const onSubmit: SubmitHandler = useCallback(() => { if (ruleId !== '') { diff --git a/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/utils.test.ts b/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/utils.test.ts index 03bd297e1fc..bb0a0e2c76d 100644 --- a/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/utils.test.ts +++ b/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/utils.test.ts @@ -14,9 +14,10 @@ // // SPDX-License-Identifier: Apache-2.0 +import { SeedTypeCypher, SeedTypeObjectId } from 'js-client-library'; import { cloneDeep } from 'lodash'; import { errorSilencer } from '../../../mocks/stderr'; -import { handleError } from './utils'; +import { getErrorMessage, handleError } from './utils'; const mockAxiosError = { isAxiosError: true, @@ -116,4 +117,93 @@ describe('handleError', () => { notificationOptions ); }); + + it('reports an Object ID-specific error when ruleType is SeedTypeObjectId and seeds are required', () => { + const expectedMessage = 'To create a rule using Object ID, add at least one object using the field below.'; + + const handleErrorSpy = vi.fn(); + handleError(mockAxiosCypherError, 'creating', 'rule', handleErrorSpy, { ruleType: SeedTypeObjectId }); + expect(handleErrorSpy).toHaveBeenCalledWith( + expectedMessage, + 'privilege-zones_creating-rule', + notificationOptions + ); + }); + + it('reports a Cypher-specific error when ruleType is SeedTypeCypher and seeds are required', () => { + const expectedMessage = + 'To save a rule created using Cypher, the Cypher must be run first. Click "Run" to continue'; + + const handleErrorSpy = vi.fn(); + handleError(mockAxiosCypherError, 'creating', 'rule', handleErrorSpy, { ruleType: SeedTypeCypher }); + expect(handleErrorSpy).toHaveBeenCalledWith( + expectedMessage, + 'privilege-zones_creating-rule', + notificationOptions + ); + }); + + it('falls back to the default Cypher message when ruleType is undefined and seeds are required', () => { + const expectedMessage = + 'To save a rule created using Cypher, the Cypher must be run first. Click "Run" to continue'; + + const handleErrorSpy = vi.fn(); + handleError(mockAxiosCypherError, 'creating', 'rule', handleErrorSpy); + expect(handleErrorSpy).toHaveBeenCalledWith( + expectedMessage, + 'privilege-zones_creating-rule', + notificationOptions + ); + }); + + it('passes ruleType through optionalParams with empty object', () => { + const handleErrorSpy = vi.fn(); + handleError(mockAxiosCypherError, 'creating', 'rule', handleErrorSpy, {}); + // No ruleType provided, should fall back to default Cypher message + expect(handleErrorSpy).toHaveBeenCalledWith( + 'To save a rule created using Cypher, the Cypher must be run first. Click "Run" to continue', + 'privilege-zones_creating-rule', + notificationOptions + ); + }); +}); + +describe('getErrorMessage', () => { + it('returns name uniqueness message for "name must be unique"', () => { + const result = getErrorMessage('name must be unique', 'creating', 'rule'); + expect(result).toBe( + 'Error creating rule: rule names must be unique. Please provide a unique name for your new rule and try again.' + ); + }); + + it('returns Object ID message when ruleType is SeedTypeObjectId and seeds are required', () => { + const result = getErrorMessage('seeds are required', 'creating', 'rule', SeedTypeObjectId); + expect(result).toContain('Object ID'); + expect(result).toBe('To create a rule using Object ID, add at least one object using the field below.'); + }); + + it('returns Cypher message when ruleType is SeedTypeCypher and seeds are required', () => { + const result = getErrorMessage('seeds are required', 'creating', 'rule', SeedTypeCypher); + expect(result).toContain('Cypher'); + expect(result).toContain('Click "Run" to continue'); + }); + + it('returns fallback Cypher message when ruleType is undefined and seeds are required', () => { + const result = getErrorMessage('seeds are required', 'creating', 'rule'); + expect(result).toBe( + 'To save a rule created using Cypher, the Cypher must be run first. Click "Run" to continue' + ); + }); + + it('returns default error message for unknown API messages', () => { + const result = getErrorMessage('something unexpected', 'updating', 'zone'); + expect(result).toBe( + 'An unexpected error occurred while updating the zone. Message: something unexpected. Please try again.' + ); + }); + + it('uses the correct entity name in the Object ID message', () => { + const result = getErrorMessage('seeds are required', 'updating', 'zone', SeedTypeObjectId); + expect(result).toBe('To create a zone using Object ID, add at least one object using the field below.'); + }); }); diff --git a/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/utils.ts b/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/utils.ts index 0c41d079d08..1e59fee7747 100644 --- a/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/utils.ts +++ b/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Save/utils.ts @@ -14,15 +14,20 @@ // // SPDX-License-Identifier: Apache-2.0 -import { isAxiosError } from 'js-client-library'; +import { isAxiosError, SeedTypeCypher, SeedTypeObjectId, SeedTypes, SeedTypesMap } from 'js-client-library'; import { OptionsObject } from 'notistack'; -export const getErrorMessage = (apiMessage: string, action: string, entity: string) => { +export const getErrorMessage = (apiMessage: string, action: string, entity: string, ruleType?: SeedTypes) => { switch (apiMessage) { case 'name must be unique': return `Error ${action} ${entity}: ${entity} names must be unique. Please provide a unique name for your new ${entity} and try again.`; case 'seeds are required': + if (ruleType === SeedTypeObjectId) { + return `To create a ${entity} using ${SeedTypesMap[SeedTypeObjectId]}, add at least one object using the field below.`; + } else if (ruleType === SeedTypeCypher) { + return `To save a ${entity} created using ${SeedTypesMap[SeedTypeCypher]}, the ${SeedTypesMap[SeedTypeCypher]} must be run first. Click "Run" to continue`; + } return `To save a ${entity} created using Cypher, the Cypher must be run first. Click "Run" to continue`; default: @@ -34,7 +39,8 @@ export const handleError = ( error: unknown, action: 'creating' | 'updating' | 'deleting', entity: 'rule' | 'zone' | 'label', - addNotification: (notification: string, key?: string, options?: OptionsObject) => void + addNotification: (notification: string, key?: string, options?: OptionsObject) => void, + optionalParams?: { ruleType?: SeedTypes } ) => { console.error(error); @@ -49,7 +55,7 @@ export const handleError = ( const apiMessage = errorsList.length ? errorsList[0].message : error.response?.statusText || undefined; if (apiMessage) { - message = getErrorMessage(apiMessage, action, entity); + message = getErrorMessage(apiMessage, action, entity, optionalParams?.ruleType); } }