mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security AI Assistant] Centralized Anonymization (#179701)
PR changes includes migration for anonymization defaults from the local
storage to use the ES data stream instead.
1. Extended `AIAssistantService` with the default setup method
`createDefaultAnonymizationFields` for anonymization fields, which will
be executed once for each space data stream. With that approach we will
have anonymization settings centralized for all the applications which
will use security GenAI functionality.
2. Anonymization fields is not personalizable and all the changes will
be applied to all users in the current space. Only users with the
defined privileges will be able to change anonymization setting.
3. Removed anonymization defaults from the AssistantContext provider.
Now fetch anonymization fields using API
`"/api/elastic_assistant/anonymization_fields/_find"`.
4. Anonymization settings component:
- replaced usage of `defaultAllow: string[];`, `defaultAllowReplacement:
string[];`,
`setUpdatedDefaultAllow:React.Dispatch<React.SetStateAction<string[]>>;`,
`setUpdatedDefaultAllowReplacement:
React.Dispatch<React.SetStateAction<string[]>>;` with
anonymizationFields fetched from the APIs;
- for the anonymize/allow UI operations use bulk actions update
operation;
- all the changes will be send to the server side using bulk API
`"/api/elastic_assistant/anonymization_fields/_bulk_action" ` after Save
settings button clicked or reset to the previous state when clicked
Cancel.
5. Data anonymization editor:
- This component will remain to apply the anonymization changes only in
the message scope. That means that any type of changes will be persisted
in the `.kibana-elastic-ai-assistant-anonymization-fields` and impact
only on the fields replacements in the selectedPromptContext.
6. Changed `.kibana-elastic-ai-assistant-anonymization-fields` data
stream schema to fits the UX:
```
"@timestamp": "2024-04-05T04:10:24.764Z",
"created_at": "2024-04-05T04:10:24.764Z",
"created_by": "",
"field": "_id",
"anonymized": false,
"allowed": true,
"namespace": "default",
"updated_at": "2024-04-05T17:15:00.903Z"
```
Components involved:
<img width="1091" alt="Screenshot 2024-04-05 at 11 35 05 AM" src="807ee596
-101f-413c-97d6-8f62c723397e">
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
This commit is contained in:
parent
8da3ec4413
commit
97448bd65a
119 changed files with 1835 additions and 1879 deletions
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION = '2023-10-31';
|
||||
export const ELASTIC_AI_ASSISTANT_INTERNAL_API_VERSION = '1';
|
||||
|
||||
export const ELASTIC_AI_ASSISTANT_URL = '/api/elastic_assistant';
|
||||
|
|
|
@ -19,8 +19,32 @@ describe('getAnonymizedData', () => {
|
|||
};
|
||||
|
||||
const commonArgs = {
|
||||
allow: ['doNotReplace', 'empty', 'host.ip', 'host.name'],
|
||||
allowReplacement: ['empty', 'host.ip', 'host.name'],
|
||||
anonymizationFields: [
|
||||
{
|
||||
id: 'doNotReplace',
|
||||
field: 'doNotReplace',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
id: 'empty',
|
||||
field: 'empty',
|
||||
anonymized: true,
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
id: 'host.ip',
|
||||
field: 'host.ip',
|
||||
anonymized: true,
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
id: 'host.name',
|
||||
field: 'host.name',
|
||||
anonymized: true,
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
currentReplacements: {},
|
||||
rawData,
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
|
|
|
@ -4,20 +4,18 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AnonymizationFieldResponse } from '../../schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { isAllowed } from '../helpers';
|
||||
import type { AnonymizedData, GetAnonymizedValues } from '../types';
|
||||
|
||||
export const getAnonymizedData = ({
|
||||
allow,
|
||||
allowReplacement,
|
||||
anonymizationFields,
|
||||
currentReplacements,
|
||||
getAnonymizedValue,
|
||||
getAnonymizedValues,
|
||||
rawData,
|
||||
}: {
|
||||
allow: string[];
|
||||
allowReplacement: string[];
|
||||
anonymizationFields?: AnonymizationFieldResponse[];
|
||||
currentReplacements: Record<string, string> | undefined;
|
||||
getAnonymizedValue: ({
|
||||
currentReplacements,
|
||||
|
@ -31,13 +29,9 @@ export const getAnonymizedData = ({
|
|||
}): AnonymizedData =>
|
||||
Object.keys(rawData).reduce<AnonymizedData>(
|
||||
(acc, field) => {
|
||||
const allowReplacementSet = new Set(allowReplacement);
|
||||
const allowSet = new Set(allow);
|
||||
|
||||
if (isAllowed({ allowSet, field })) {
|
||||
if (isAllowed({ anonymizationFields: anonymizationFields ?? [], field })) {
|
||||
const { anonymizedValues, replacements } = getAnonymizedValues({
|
||||
allowReplacementSet,
|
||||
allowSet,
|
||||
anonymizationFields,
|
||||
currentReplacements,
|
||||
field,
|
||||
getAnonymizedValue,
|
||||
|
|
|
@ -11,8 +11,7 @@ import { mockGetAnonymizedValue } from '../../mock/get_anonymized_value';
|
|||
describe('getAnonymizedValues', () => {
|
||||
it('returns empty anonymizedValues and replacements when provided with empty raw data', () => {
|
||||
const result = getAnonymizedValues({
|
||||
allowReplacementSet: new Set(),
|
||||
allowSet: new Set(),
|
||||
anonymizationFields: [],
|
||||
currentReplacements: {},
|
||||
field: 'test.field',
|
||||
getAnonymizedValue: jest.fn(),
|
||||
|
@ -31,8 +30,9 @@ describe('getAnonymizedValues', () => {
|
|||
};
|
||||
|
||||
const result = getAnonymizedValues({
|
||||
allowReplacementSet: new Set(['test.field']),
|
||||
allowSet: new Set(['test.field']),
|
||||
anonymizationFields: [
|
||||
{ id: 'test.field', field: 'test.field', allowed: true, anonymized: true },
|
||||
],
|
||||
currentReplacements: {},
|
||||
field: 'test.field',
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
|
@ -48,8 +48,9 @@ describe('getAnonymizedValues', () => {
|
|||
};
|
||||
|
||||
const result = getAnonymizedValues({
|
||||
allowReplacementSet: new Set(['test.field']),
|
||||
allowSet: new Set(['test.field']),
|
||||
anonymizationFields: [
|
||||
{ id: 'test.field', field: 'test.field', allowed: true, anonymized: true },
|
||||
],
|
||||
currentReplacements: {},
|
||||
field: 'test.field',
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
|
@ -68,8 +69,9 @@ describe('getAnonymizedValues', () => {
|
|||
};
|
||||
|
||||
const result = getAnonymizedValues({
|
||||
allowReplacementSet: new Set(), // does NOT include `test.field`
|
||||
allowSet: new Set(['test.field']),
|
||||
anonymizationFields: [
|
||||
{ id: 'test.field', field: 'test.field', allowed: true, anonymized: false },
|
||||
],
|
||||
currentReplacements: {},
|
||||
field: 'test.field',
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
|
@ -85,8 +87,9 @@ describe('getAnonymizedValues', () => {
|
|||
};
|
||||
|
||||
const result = getAnonymizedValues({
|
||||
allowReplacementSet: new Set(['test.field']),
|
||||
allowSet: new Set(), // does NOT include `test.field`
|
||||
anonymizationFields: [
|
||||
{ id: 'test.field', field: 'test.field', allowed: false, anonymized: true },
|
||||
],
|
||||
currentReplacements: {},
|
||||
field: 'test.field',
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
|
|
|
@ -9,8 +9,7 @@ import { isAllowed, isAnonymized } from '../helpers';
|
|||
import { AnonymizedValues, GetAnonymizedValues } from '../types';
|
||||
|
||||
export const getAnonymizedValues: GetAnonymizedValues = ({
|
||||
allowSet,
|
||||
allowReplacementSet,
|
||||
anonymizationFields = [],
|
||||
currentReplacements,
|
||||
field,
|
||||
getAnonymizedValue,
|
||||
|
@ -22,7 +21,10 @@ export const getAnonymizedValues: GetAnonymizedValues = ({
|
|||
(acc, rawValue) => {
|
||||
const stringValue = `${rawValue}`;
|
||||
|
||||
if (isAllowed({ allowSet, field }) && isAnonymized({ allowReplacementSet, field })) {
|
||||
if (
|
||||
isAllowed({ anonymizationFields, field }) &&
|
||||
isAnonymized({ anonymizationFields, field })
|
||||
) {
|
||||
const anonymizedValue = `${getAnonymizedValue({
|
||||
currentReplacements,
|
||||
rawValue: stringValue,
|
||||
|
@ -35,7 +37,7 @@ export const getAnonymizedValues: GetAnonymizedValues = ({
|
|||
[anonymizedValue]: stringValue,
|
||||
},
|
||||
};
|
||||
} else if (isAllowed({ allowSet, field })) {
|
||||
} else if (isAllowed({ anonymizationFields, field })) {
|
||||
return {
|
||||
anonymizedValues: [...acc.anonymizedValues, stringValue], // no anonymization for this value
|
||||
replacements: {
|
||||
|
|
|
@ -7,6 +7,12 @@
|
|||
|
||||
import { isAllowed, isAnonymized, isDenied, getIsDataAnonymizable } from '.';
|
||||
|
||||
const anonymizationFields = [
|
||||
{ id: 'fieldName1', field: 'fieldName1', allowed: true, anonymized: false },
|
||||
{ id: 'fieldName2', field: 'fieldName2', allowed: false, anonymized: false },
|
||||
{ id: 'fieldName3', field: 'fieldName3', allowed: false, anonymized: false },
|
||||
];
|
||||
|
||||
describe('helpers', () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
|
@ -30,68 +36,59 @@ describe('helpers', () => {
|
|||
|
||||
describe('isAllowed', () => {
|
||||
it('returns true when the field is present in the allowSet', () => {
|
||||
const allowSet = new Set(['fieldName1', 'fieldName2', 'fieldName3']);
|
||||
|
||||
expect(isAllowed({ allowSet, field: 'fieldName1' })).toBe(true);
|
||||
expect(isAllowed({ anonymizationFields, field: 'fieldName1' })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the field is NOT present in the allowSet', () => {
|
||||
const allowSet = new Set(['fieldName1', 'fieldName2', 'fieldName3']);
|
||||
|
||||
expect(isAllowed({ allowSet, field: 'nonexistentField' })).toBe(false);
|
||||
expect(isAllowed({ anonymizationFields, field: 'nonexistentField' })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDenied', () => {
|
||||
it('returns true when the field is NOT in the allowSet', () => {
|
||||
const allowSet = new Set(['field1', 'field2']);
|
||||
const field = 'field3';
|
||||
|
||||
expect(isDenied({ allowSet, field })).toBe(true);
|
||||
expect(isDenied({ anonymizationFields, field: 'field3' })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the field is in the allowSet', () => {
|
||||
const allowSet = new Set(['field1', 'field2']);
|
||||
const field = 'field1';
|
||||
|
||||
expect(isDenied({ allowSet, field })).toBe(false);
|
||||
expect(isDenied({ anonymizationFields, field: 'fieldName1' })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for an empty allowSet', () => {
|
||||
const allowSet = new Set<string>();
|
||||
const field = 'field1';
|
||||
|
||||
expect(isDenied({ allowSet, field })).toBe(true);
|
||||
expect(isDenied({ anonymizationFields: [], field: 'field1' })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the field is an empty string and allowSet contains the empty string', () => {
|
||||
const allowSet = new Set(['', 'field1']);
|
||||
const field = '';
|
||||
|
||||
expect(isDenied({ allowSet, field })).toBe(false);
|
||||
expect(
|
||||
isDenied({
|
||||
anonymizationFields: [
|
||||
...anonymizationFields,
|
||||
{ id: '', field: '', allowed: true, anonymized: false },
|
||||
],
|
||||
field: '',
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAnonymized', () => {
|
||||
const allowReplacementSet = new Set(['user.name', 'host.name']);
|
||||
|
||||
it('returns true when the field is in the allowReplacementSet', () => {
|
||||
const field = 'user.name';
|
||||
|
||||
expect(isAnonymized({ allowReplacementSet, field })).toBe(true);
|
||||
expect(
|
||||
isAnonymized({
|
||||
anonymizationFields: [
|
||||
...anonymizationFields,
|
||||
{ id: 'user.name', field: 'user.name', allowed: false, anonymized: true },
|
||||
],
|
||||
field: 'user.name',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the field is NOT in the allowReplacementSet', () => {
|
||||
const field = 'foozle';
|
||||
|
||||
expect(isAnonymized({ allowReplacementSet, field })).toBe(false);
|
||||
expect(isAnonymized({ anonymizationFields, field: 'foozle' })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when allowReplacementSet is empty', () => {
|
||||
const emptySet = new Set<string>();
|
||||
const field = 'user.name';
|
||||
|
||||
expect(isAnonymized({ allowReplacementSet: emptySet, field })).toBe(false);
|
||||
expect(isAnonymized({ anonymizationFields: [], field: 'user.name' })).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,23 +6,34 @@
|
|||
*/
|
||||
|
||||
import { Replacements } from '../../schemas';
|
||||
import { AnonymizationFieldResponse } from '../../schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
|
||||
export const getIsDataAnonymizable = (rawData: string | Record<string, string[]>): boolean =>
|
||||
typeof rawData !== 'string';
|
||||
|
||||
export const isAllowed = ({ allowSet, field }: { allowSet: Set<string>; field: string }): boolean =>
|
||||
allowSet.has(field);
|
||||
|
||||
export const isDenied = ({ allowSet, field }: { allowSet: Set<string>; field: string }): boolean =>
|
||||
!allowSet.has(field);
|
||||
|
||||
export const isAnonymized = ({
|
||||
allowReplacementSet,
|
||||
export const isAllowed = ({
|
||||
anonymizationFields,
|
||||
field,
|
||||
}: {
|
||||
allowReplacementSet: Set<string>;
|
||||
anonymizationFields: AnonymizationFieldResponse[];
|
||||
field: string;
|
||||
}): boolean => allowReplacementSet.has(field);
|
||||
}): boolean => anonymizationFields.find((a) => a.field === field)?.allowed ?? false;
|
||||
|
||||
export const isDenied = ({
|
||||
anonymizationFields,
|
||||
field,
|
||||
}: {
|
||||
anonymizationFields: AnonymizationFieldResponse[];
|
||||
field: string;
|
||||
}): boolean => !(anonymizationFields.find((a) => a.field === field)?.allowed ?? false);
|
||||
|
||||
export const isAnonymized = ({
|
||||
anonymizationFields,
|
||||
field,
|
||||
}: {
|
||||
anonymizationFields: AnonymizationFieldResponse[];
|
||||
field: string;
|
||||
}): boolean => anonymizationFields.find((a) => a.field === field)?.anonymized ?? false;
|
||||
|
||||
export const replaceAnonymizedValuesWithOriginalValues = ({
|
||||
messageContent,
|
||||
|
|
|
@ -10,16 +10,18 @@ import { transformRawData } from '.';
|
|||
|
||||
describe('transformRawData', () => {
|
||||
it('returns non-anonymized data when rawData is a string', () => {
|
||||
const anonymizationFields = [
|
||||
{ id: 'field1', field: 'field1', allowed: true, anonymized: true },
|
||||
{ id: 'field2', field: 'field2', allowed: false, anonymized: true },
|
||||
];
|
||||
const inputRawData = {
|
||||
allow: ['field1'],
|
||||
allowReplacement: ['field1', 'field2'],
|
||||
anonymizationFields,
|
||||
promptContextId: 'abcd',
|
||||
rawData: 'this will not be anonymized',
|
||||
};
|
||||
|
||||
const result = transformRawData({
|
||||
allow: inputRawData.allow,
|
||||
allowReplacement: inputRawData.allowReplacement,
|
||||
anonymizationFields: inputRawData.anonymizationFields,
|
||||
currentReplacements: {},
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
onNewReplacements: () => {},
|
||||
|
@ -30,9 +32,14 @@ describe('transformRawData', () => {
|
|||
});
|
||||
|
||||
it('calls onNewReplacements with the expected replacements', () => {
|
||||
const anonymizationFields = {
|
||||
total: 2,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [{ id: 'field1', field: 'field1', allowed: true, anonymized: true }],
|
||||
};
|
||||
const inputRawData = {
|
||||
allow: ['field1'],
|
||||
allowReplacement: ['field1'],
|
||||
anonymizationFields,
|
||||
promptContextId: 'abcd',
|
||||
rawData: { field1: ['value1'] },
|
||||
};
|
||||
|
@ -40,8 +47,7 @@ describe('transformRawData', () => {
|
|||
const onNewReplacements = jest.fn();
|
||||
|
||||
transformRawData({
|
||||
allow: inputRawData.allow,
|
||||
allowReplacement: inputRawData.allowReplacement,
|
||||
anonymizationFields: inputRawData.anonymizationFields.data,
|
||||
currentReplacements: {},
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
onNewReplacements,
|
||||
|
@ -52,16 +58,23 @@ describe('transformRawData', () => {
|
|||
});
|
||||
|
||||
it('returns the expected mix of anonymized and non-anonymized data as a CSV string', () => {
|
||||
const anonymizationFields = {
|
||||
total: 2,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{ id: 'field1', field: 'field1', allowed: true, anonymized: true },
|
||||
{ id: 'field2', field: 'field2', allowed: true, anonymized: false },
|
||||
],
|
||||
};
|
||||
const inputRawData = {
|
||||
allow: ['field1', 'field2'],
|
||||
allowReplacement: ['field1'], // only field 1 will be anonymized
|
||||
anonymizationFields, // only field 1 will be anonymized
|
||||
promptContextId: 'abcd',
|
||||
rawData: { field1: ['value1', 'value2'], field2: ['value3', 'value4'] },
|
||||
};
|
||||
|
||||
const result = transformRawData({
|
||||
allow: inputRawData.allow,
|
||||
allowReplacement: inputRawData.allowReplacement,
|
||||
anonymizationFields: inputRawData.anonymizationFields.data,
|
||||
currentReplacements: {},
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
onNewReplacements: () => {},
|
||||
|
@ -72,9 +85,18 @@ describe('transformRawData', () => {
|
|||
});
|
||||
|
||||
it('omits fields that are not included in the `allow` list, even if they are members of `allowReplacement`', () => {
|
||||
const anonymizationFields = {
|
||||
total: 2,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{ id: 'field1', field: 'field1', allowed: true, anonymized: true },
|
||||
{ id: 'field2', field: 'field2', allowed: true, anonymized: false },
|
||||
{ id: 'field3', field: 'field3', allowed: false, anonymized: true },
|
||||
],
|
||||
};
|
||||
const inputRawData = {
|
||||
allow: ['field1', 'field2'], // field3 is NOT allowed
|
||||
allowReplacement: ['field1', 'field3'], // field3 is requested to be anonymized
|
||||
anonymizationFields, // field3 is requested to be anonymized
|
||||
promptContextId: 'abcd',
|
||||
rawData: {
|
||||
field1: ['value1', 'value2'],
|
||||
|
@ -84,8 +106,7 @@ describe('transformRawData', () => {
|
|||
};
|
||||
|
||||
const result = transformRawData({
|
||||
allow: inputRawData.allow,
|
||||
allowReplacement: inputRawData.allowReplacement,
|
||||
anonymizationFields: inputRawData.anonymizationFields.data,
|
||||
currentReplacements: {},
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
onNewReplacements: () => {},
|
||||
|
|
|
@ -6,20 +6,19 @@
|
|||
*/
|
||||
|
||||
import { Replacements } from '../../schemas';
|
||||
import { AnonymizationFieldResponse } from '../../schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { getAnonymizedData } from '../get_anonymized_data';
|
||||
import { getAnonymizedValues } from '../get_anonymized_values';
|
||||
import { getCsvFromData } from '../get_csv_from_data';
|
||||
|
||||
export const transformRawData = ({
|
||||
allow,
|
||||
allowReplacement,
|
||||
anonymizationFields,
|
||||
currentReplacements,
|
||||
getAnonymizedValue,
|
||||
onNewReplacements,
|
||||
rawData,
|
||||
}: {
|
||||
allow: string[];
|
||||
allowReplacement: string[];
|
||||
anonymizationFields?: AnonymizationFieldResponse[];
|
||||
currentReplacements: Replacements | undefined;
|
||||
getAnonymizedValue: ({
|
||||
currentReplacements,
|
||||
|
@ -36,8 +35,7 @@ export const transformRawData = ({
|
|||
}
|
||||
|
||||
const anonymizedData = getAnonymizedData({
|
||||
allow,
|
||||
allowReplacement,
|
||||
anonymizationFields,
|
||||
currentReplacements,
|
||||
rawData,
|
||||
getAnonymizedValue,
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AnonymizationFieldResponse } from '../schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
|
||||
export interface AnonymizedValues {
|
||||
/** The original values were transformed to these anonymized values */
|
||||
anonymizedValues: string[];
|
||||
|
@ -22,15 +24,13 @@ export interface AnonymizedData {
|
|||
}
|
||||
|
||||
export type GetAnonymizedValues = ({
|
||||
allowReplacementSet,
|
||||
allowSet,
|
||||
anonymizationFields,
|
||||
currentReplacements,
|
||||
field,
|
||||
getAnonymizedValue,
|
||||
rawData,
|
||||
}: {
|
||||
allowReplacementSet: Set<string>;
|
||||
allowSet: Set<string>;
|
||||
anonymizationFields?: AnonymizationFieldResponse[];
|
||||
currentReplacements: Record<string, string> | undefined;
|
||||
field: string;
|
||||
getAnonymizedValue: ({
|
||||
|
|
|
@ -16,7 +16,7 @@ import { z } from 'zod';
|
|||
* version: 2023-10-31
|
||||
*/
|
||||
|
||||
import { UUID, NonEmptyString, User } from '../conversations/common_attributes.gen';
|
||||
import { UUID, NonEmptyString } from '../conversations/common_attributes.gen';
|
||||
|
||||
export type BulkActionSkipReason = z.infer<typeof BulkActionSkipReason>;
|
||||
export const BulkActionSkipReason = z.literal('ANONYMIZATION_FIELD_NOT_MODIFIED');
|
||||
|
@ -47,13 +47,12 @@ export const AnonymizationFieldResponse = z.object({
|
|||
id: UUID,
|
||||
timestamp: NonEmptyString.optional(),
|
||||
field: z.string(),
|
||||
defaultAllow: z.boolean().optional(),
|
||||
defaultAllowReplacement: z.boolean().optional(),
|
||||
allowed: z.boolean().optional(),
|
||||
anonymized: z.boolean().optional(),
|
||||
updatedAt: z.string().optional(),
|
||||
updatedBy: z.string().optional(),
|
||||
createdAt: z.string().optional(),
|
||||
createdBy: z.string().optional(),
|
||||
users: z.array(User).optional(),
|
||||
/**
|
||||
* Kibana space
|
||||
*/
|
||||
|
@ -104,15 +103,15 @@ export const BulkActionBase = z.object({
|
|||
export type AnonymizationFieldCreateProps = z.infer<typeof AnonymizationFieldCreateProps>;
|
||||
export const AnonymizationFieldCreateProps = z.object({
|
||||
field: z.string(),
|
||||
defaultAllow: z.boolean().optional(),
|
||||
defaultAllowReplacement: z.boolean().optional(),
|
||||
allowed: z.boolean().optional(),
|
||||
anonymized: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type AnonymizationFieldUpdateProps = z.infer<typeof AnonymizationFieldUpdateProps>;
|
||||
export const AnonymizationFieldUpdateProps = z.object({
|
||||
id: z.string(),
|
||||
defaultAllow: z.boolean().optional(),
|
||||
defaultAllowReplacement: z.boolean().optional(),
|
||||
allowed: z.boolean().optional(),
|
||||
anonymized: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type PerformBulkActionRequestBody = z.infer<typeof PerformBulkActionRequestBody>;
|
||||
|
|
|
@ -108,9 +108,9 @@ components:
|
|||
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/NonEmptyString'
|
||||
field:
|
||||
type: string
|
||||
defaultAllow:
|
||||
allowed:
|
||||
type: boolean
|
||||
defaultAllowReplacement:
|
||||
anonymized:
|
||||
type: boolean
|
||||
updatedAt:
|
||||
type: string
|
||||
|
@ -120,10 +120,6 @@ components:
|
|||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/User'
|
||||
namespace:
|
||||
type: string
|
||||
description: Kibana space
|
||||
|
@ -220,9 +216,9 @@ components:
|
|||
properties:
|
||||
field:
|
||||
type: string
|
||||
defaultAllow:
|
||||
allowed:
|
||||
type: boolean
|
||||
defaultAllowReplacement:
|
||||
anonymized:
|
||||
type: boolean
|
||||
|
||||
AnonymizationFieldUpdateProps:
|
||||
|
@ -232,8 +228,8 @@ components:
|
|||
properties:
|
||||
id:
|
||||
type: string
|
||||
defaultAllow:
|
||||
allowed:
|
||||
type: boolean
|
||||
defaultAllowReplacement:
|
||||
anonymized:
|
||||
type: boolean
|
||||
|
|
@ -22,8 +22,9 @@ import { AnonymizationFieldResponse } from './bulk_crud_anonymization_fields_rou
|
|||
export type FindAnonymizationFieldsSortField = z.infer<typeof FindAnonymizationFieldsSortField>;
|
||||
export const FindAnonymizationFieldsSortField = z.enum([
|
||||
'created_at',
|
||||
'is_default',
|
||||
'title',
|
||||
'anonymized',
|
||||
'allowed',
|
||||
'field',
|
||||
'updated_at',
|
||||
]);
|
||||
export type FindAnonymizationFieldsSortFieldEnum = typeof FindAnonymizationFieldsSortField.enum;
|
||||
|
|
|
@ -97,8 +97,9 @@ components:
|
|||
type: string
|
||||
enum:
|
||||
- 'created_at'
|
||||
- 'is_default'
|
||||
- 'title'
|
||||
- 'anonymized'
|
||||
- 'allowed'
|
||||
- 'field'
|
||||
- 'updated_at'
|
||||
|
||||
SortOrder:
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* 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 {
|
||||
API_VERSIONS,
|
||||
ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_BULK_ACTION,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
|
||||
import { IToasts } from '@kbn/core-notifications-browser';
|
||||
import { bulkUpdateAnonymizationFields } from './bulk_update_anonymization_fields';
|
||||
|
||||
const anonymizationField1 = {
|
||||
id: 'field1',
|
||||
field: 'Anonymization field 1',
|
||||
anonymized: true,
|
||||
allowed: true,
|
||||
};
|
||||
const anonymizationField2 = {
|
||||
...anonymizationField1,
|
||||
id: 'field2',
|
||||
field: 'field 2',
|
||||
};
|
||||
const toasts = {
|
||||
addError: jest.fn(),
|
||||
};
|
||||
describe('bulkUpdateAnonymizationFields', () => {
|
||||
let httpMock: ReturnType<typeof httpServiceMock.createSetupContract>;
|
||||
|
||||
beforeEach(() => {
|
||||
httpMock = httpServiceMock.createSetupContract();
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('should send a POST request with the correct parameters and receive a successful response', async () => {
|
||||
const anonymizationFieldsActions = {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: { ids: [] },
|
||||
};
|
||||
|
||||
await bulkUpdateAnonymizationFields(httpMock, anonymizationFieldsActions);
|
||||
|
||||
expect(httpMock.fetch).toHaveBeenCalledWith(
|
||||
ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_BULK_ACTION,
|
||||
{
|
||||
method: 'POST',
|
||||
version: API_VERSIONS.public.v1,
|
||||
body: JSON.stringify({
|
||||
create: [],
|
||||
update: [],
|
||||
delete: { ids: [] },
|
||||
}),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should transform the anonymization field dictionary to an array of fields to create', async () => {
|
||||
const anonymizationFieldsActions = {
|
||||
create: [anonymizationField1, anonymizationField2],
|
||||
update: [],
|
||||
delete: { ids: [] },
|
||||
};
|
||||
|
||||
await bulkUpdateAnonymizationFields(httpMock, anonymizationFieldsActions);
|
||||
|
||||
expect(httpMock.fetch).toHaveBeenCalledWith(
|
||||
ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_BULK_ACTION,
|
||||
{
|
||||
method: 'POST',
|
||||
version: API_VERSIONS.public.v1,
|
||||
body: JSON.stringify({
|
||||
create: [anonymizationField1, anonymizationField2],
|
||||
update: [],
|
||||
delete: { ids: [] },
|
||||
}),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should transform the anonymization field dictionary to an array of fields to update', async () => {
|
||||
const anonymizationFieldsActions = {
|
||||
update: [anonymizationField1, anonymizationField2],
|
||||
delete: { ids: [] },
|
||||
};
|
||||
|
||||
await bulkUpdateAnonymizationFields(httpMock, anonymizationFieldsActions);
|
||||
|
||||
expect(httpMock.fetch).toHaveBeenCalledWith(
|
||||
ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_BULK_ACTION,
|
||||
{
|
||||
method: 'POST',
|
||||
version: API_VERSIONS.public.v1,
|
||||
body: JSON.stringify({
|
||||
update: [anonymizationField1, anonymizationField2],
|
||||
delete: { ids: [] },
|
||||
}),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error with the correct message when receiving an unsuccessful response', async () => {
|
||||
httpMock.fetch.mockResolvedValue({
|
||||
success: false,
|
||||
attributes: {
|
||||
errors: [
|
||||
{
|
||||
statusCode: 400,
|
||||
message: 'Error updating anonymization field',
|
||||
anonymization_fields: [{ id: anonymizationField1.id, name: anonymizationField1.field }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const anonymizationFieldsActions = {
|
||||
create: [],
|
||||
update: [anonymizationField1],
|
||||
delete: { ids: [] },
|
||||
};
|
||||
await bulkUpdateAnonymizationFields(
|
||||
httpMock,
|
||||
anonymizationFieldsActions,
|
||||
toasts as unknown as IToasts
|
||||
);
|
||||
expect(toasts.addError.mock.calls[0][0]).toEqual(
|
||||
new Error(
|
||||
'Error message: Error updating anonymization field for anonymization field Anonymization field 1'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle cases where result.attributes.errors is undefined', async () => {
|
||||
httpMock.fetch.mockResolvedValue({
|
||||
success: false,
|
||||
attributes: {},
|
||||
});
|
||||
const anonymizationFieldsActions = {
|
||||
create: [],
|
||||
update: [],
|
||||
delete: { ids: [] },
|
||||
};
|
||||
|
||||
await bulkUpdateAnonymizationFields(
|
||||
httpMock,
|
||||
anonymizationFieldsActions,
|
||||
toasts as unknown as IToasts
|
||||
);
|
||||
expect(toasts.addError.mock.calls[0][0]).toEqual(new Error(''));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { HttpSetup, IToasts } from '@kbn/core/public';
|
||||
import {
|
||||
ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_BULK_ACTION,
|
||||
API_VERSIONS,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import {
|
||||
PerformBulkActionRequestBody,
|
||||
PerformBulkActionResponse,
|
||||
} from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
|
||||
export const bulkUpdateAnonymizationFields = async (
|
||||
http: HttpSetup,
|
||||
anonymizationFieldsActions: PerformBulkActionRequestBody,
|
||||
toasts?: IToasts
|
||||
) => {
|
||||
try {
|
||||
const result = await http.fetch<PerformBulkActionResponse>(
|
||||
ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_BULK_ACTION,
|
||||
{
|
||||
method: 'POST',
|
||||
version: API_VERSIONS.public.v1,
|
||||
body: JSON.stringify(anonymizationFieldsActions),
|
||||
}
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
const serverError = result.attributes.errors
|
||||
?.map(
|
||||
(e) =>
|
||||
`${e.status_code ? `Error code: ${e.status_code}. ` : ''}Error message: ${
|
||||
e.message
|
||||
} for anonymization field ${e.anonymization_fields.map((c) => c.name).join(',')}`
|
||||
)
|
||||
.join(',\n');
|
||||
throw new Error(serverError);
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, {
|
||||
title: i18n.translate(
|
||||
'xpack.elasticAssistant.anonymizationFields.bulkActionsAnonymizationFieldsError',
|
||||
{
|
||||
defaultMessage: 'Error updating anonymization fields {error}',
|
||||
values: { error },
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
UseFetchAnonymizationFieldsParams,
|
||||
useFetchAnonymizationFields,
|
||||
} from './use_fetch_anonymization_fields';
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
|
||||
const statusResponse = { assistantModelEvaluation: true, assistantStreamingEnabled: false };
|
||||
|
||||
const http = {
|
||||
fetch: jest.fn().mockResolvedValue(statusResponse),
|
||||
} as unknown as HttpSetup;
|
||||
|
||||
const defaultProps = {
|
||||
http,
|
||||
isAssistantEnabled: true,
|
||||
} as unknown as UseFetchAnonymizationFieldsParams;
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient();
|
||||
// eslint-disable-next-line react/display-name
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('useFetchAnonymizationFields', () => {
|
||||
it(`should make http request to fetch anonymization fields`, async () => {
|
||||
renderHook(() => useFetchAnonymizationFields(defaultProps), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() => useFetchAnonymizationFields(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
expect(defaultProps.http.fetch).toHaveBeenCalledWith(
|
||||
'/api/elastic_assistant/anonymization_fields/_find',
|
||||
{
|
||||
method: 'GET',
|
||||
query: {
|
||||
page: 1,
|
||||
per_page: 1000,
|
||||
},
|
||||
version: '2023-10-31',
|
||||
signal: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
expect(defaultProps.http.fetch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
API_VERSIONS,
|
||||
ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_FIND,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
|
||||
export interface UseFetchAnonymizationFieldsParams {
|
||||
http: HttpSetup;
|
||||
isAssistantEnabled: boolean;
|
||||
signal?: AbortSignal | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* API call for fetching anonymization fields for current spaceId
|
||||
*
|
||||
* @param {Object} options - The options object.
|
||||
* @param {HttpSetup} options.http - HttpSetup
|
||||
* @param {AbortSignal} [options.signal] - AbortSignal
|
||||
*
|
||||
* @returns {useQuery} hook for getting the status of the anonymization fields
|
||||
*/
|
||||
export const useFetchAnonymizationFields = ({
|
||||
http,
|
||||
signal,
|
||||
isAssistantEnabled,
|
||||
}: UseFetchAnonymizationFieldsParams) => {
|
||||
const query = {
|
||||
page: 1,
|
||||
per_page: 1000, // Continue use in-memory paging till the new design will be ready
|
||||
};
|
||||
|
||||
const cachingKeys = [
|
||||
ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_FIND,
|
||||
query.page,
|
||||
query.per_page,
|
||||
API_VERSIONS.public.v1,
|
||||
];
|
||||
|
||||
return useQuery([cachingKeys, query], async () => {
|
||||
if (!isAssistantEnabled) {
|
||||
return { page: 0, perPage: 0, total: 0, data: [] };
|
||||
}
|
||||
const res = await http.fetch<FindAnonymizationFieldsResponse>(
|
||||
ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_FIND,
|
||||
{
|
||||
method: 'GET',
|
||||
version: API_VERSIONS.public.v1,
|
||||
query,
|
||||
signal,
|
||||
}
|
||||
);
|
||||
return res;
|
||||
});
|
||||
};
|
|
@ -5,10 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { bulkChangeConversations } from './use_bulk_actions_conversations';
|
||||
import { bulkUpdateConversations } from './bulk_update_actions_conversations';
|
||||
import {
|
||||
API_VERSIONS,
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION,
|
||||
ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
|
||||
import { IToasts } from '@kbn/core-notifications-browser';
|
||||
|
@ -42,7 +42,7 @@ const conversation2 = {
|
|||
const toasts = {
|
||||
addError: jest.fn(),
|
||||
};
|
||||
describe('bulkChangeConversations', () => {
|
||||
describe('bulkUpdateConversations', () => {
|
||||
let httpMock: ReturnType<typeof httpServiceMock.createSetupContract>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -57,13 +57,13 @@ describe('bulkChangeConversations', () => {
|
|||
delete: { ids: [] },
|
||||
};
|
||||
|
||||
await bulkChangeConversations(httpMock, conversationsActions);
|
||||
await bulkUpdateConversations(httpMock, conversationsActions);
|
||||
|
||||
expect(httpMock.fetch).toHaveBeenCalledWith(
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION,
|
||||
{
|
||||
method: 'POST',
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
version: API_VERSIONS.public.v1,
|
||||
body: JSON.stringify({
|
||||
update: [],
|
||||
create: [],
|
||||
|
@ -83,13 +83,13 @@ describe('bulkChangeConversations', () => {
|
|||
delete: { ids: [] },
|
||||
};
|
||||
|
||||
await bulkChangeConversations(httpMock, conversationsActions);
|
||||
await bulkUpdateConversations(httpMock, conversationsActions);
|
||||
|
||||
expect(httpMock.fetch).toHaveBeenCalledWith(
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION,
|
||||
{
|
||||
method: 'POST',
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
version: API_VERSIONS.public.v1,
|
||||
body: JSON.stringify({
|
||||
update: [],
|
||||
create: [conversation1, conversation2],
|
||||
|
@ -108,13 +108,13 @@ describe('bulkChangeConversations', () => {
|
|||
delete: { ids: [] },
|
||||
};
|
||||
|
||||
await bulkChangeConversations(httpMock, conversationsActions);
|
||||
await bulkUpdateConversations(httpMock, conversationsActions);
|
||||
|
||||
expect(httpMock.fetch).toHaveBeenCalledWith(
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION,
|
||||
{
|
||||
method: 'POST',
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
version: API_VERSIONS.public.v1,
|
||||
body: JSON.stringify({
|
||||
update: [conversation1, conversation2],
|
||||
delete: { ids: [] },
|
||||
|
@ -141,7 +141,7 @@ describe('bulkChangeConversations', () => {
|
|||
update: {},
|
||||
delete: { ids: [] },
|
||||
};
|
||||
await bulkChangeConversations(httpMock, conversationsActions, toasts as unknown as IToasts);
|
||||
await bulkUpdateConversations(httpMock, conversationsActions, toasts as unknown as IToasts);
|
||||
expect(toasts.addError.mock.calls[0][0]).toEqual(
|
||||
new Error('Error message: Error updating conversations for conversation Conversation 1')
|
||||
);
|
||||
|
@ -158,7 +158,7 @@ describe('bulkChangeConversations', () => {
|
|||
delete: { ids: [] },
|
||||
};
|
||||
|
||||
await bulkChangeConversations(httpMock, conversationsActions, toasts as unknown as IToasts);
|
||||
await bulkUpdateConversations(httpMock, conversationsActions, toasts as unknown as IToasts);
|
||||
expect(toasts.addError.mock.calls[0][0]).toEqual(new Error(''));
|
||||
});
|
||||
});
|
|
@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n';
|
|||
import { HttpSetup, IToasts } from '@kbn/core/public';
|
||||
import {
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION,
|
||||
ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
ApiConfig,
|
||||
API_VERSIONS,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { Conversation, ClientMessage } from '../../../assistant_context/types';
|
||||
|
||||
|
@ -92,7 +92,7 @@ const transformUpdateActions = (
|
|||
[]
|
||||
);
|
||||
|
||||
export const bulkChangeConversations = async (
|
||||
export const bulkUpdateConversations = async (
|
||||
http: HttpSetup,
|
||||
conversationsActions: ConversationsBulkActions,
|
||||
toasts?: IToasts
|
||||
|
@ -114,7 +114,7 @@ export const bulkChangeConversations = async (
|
|||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION,
|
||||
{
|
||||
method: 'POST',
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
version: API_VERSIONS.public.v1,
|
||||
body: JSON.stringify({
|
||||
update: conversationsToUpdate,
|
||||
create: conversationsToCreate,
|
|
@ -9,9 +9,9 @@ import { HttpSetup, IToasts } from '@kbn/core/public';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL,
|
||||
ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
ApiConfig,
|
||||
Replacements,
|
||||
API_VERSIONS,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { Conversation, ClientMessage } from '../../../assistant_context/types';
|
||||
|
||||
|
@ -42,7 +42,7 @@ export const getConversationById = async ({
|
|||
try {
|
||||
const response = await http.fetch(`${ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL}/${id}`, {
|
||||
method: 'GET',
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
version: API_VERSIONS.public.v1,
|
||||
signal,
|
||||
});
|
||||
|
||||
|
@ -85,7 +85,7 @@ export const createConversation = async ({
|
|||
try {
|
||||
const response = await http.post(ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL, {
|
||||
body: JSON.stringify(conversation),
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
version: API_VERSIONS.public.v1,
|
||||
signal,
|
||||
});
|
||||
|
||||
|
@ -128,7 +128,7 @@ export const deleteConversation = async ({
|
|||
try {
|
||||
const response = await http.fetch(`${ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL}/${id}`, {
|
||||
method: 'DELETE',
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
version: API_VERSIONS.public.v1,
|
||||
signal,
|
||||
});
|
||||
|
||||
|
@ -197,7 +197,7 @@ export const updateConversation = async ({
|
|||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
version: API_VERSIONS.public.v1,
|
||||
signal,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -6,5 +6,5 @@
|
|||
*/
|
||||
|
||||
export * from './conversations';
|
||||
export * from './use_bulk_actions_conversations';
|
||||
export * from './bulk_update_actions_conversations';
|
||||
export * from './use_fetch_current_user_conversations';
|
||||
|
|
|
@ -22,7 +22,11 @@ const http = {
|
|||
};
|
||||
const onFetch = jest.fn();
|
||||
|
||||
const defaultProps = { http, onFetch } as unknown as UseFetchCurrentUserConversationsParams;
|
||||
const defaultProps = {
|
||||
http,
|
||||
onFetch,
|
||||
isAssistantEnabled: true,
|
||||
} as unknown as UseFetchCurrentUserConversationsParams;
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient();
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
API_VERSIONS,
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND,
|
||||
ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { Conversation } from '../../../assistant_context/types';
|
||||
|
||||
|
@ -25,6 +25,7 @@ export interface UseFetchCurrentUserConversationsParams {
|
|||
onFetch: (result: FetchConversationsResponse) => Record<string, Conversation>;
|
||||
signal?: AbortSignal | undefined;
|
||||
refetchOnWindowFocus?: boolean;
|
||||
isAssistantEnabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -42,6 +43,7 @@ export const useFetchCurrentUserConversations = ({
|
|||
onFetch,
|
||||
signal,
|
||||
refetchOnWindowFocus = true,
|
||||
isAssistantEnabled,
|
||||
}: UseFetchCurrentUserConversationsParams) => {
|
||||
const query = {
|
||||
page: 1,
|
||||
|
@ -52,17 +54,20 @@ export const useFetchCurrentUserConversations = ({
|
|||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND,
|
||||
query.page,
|
||||
query.perPage,
|
||||
ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
API_VERSIONS.public.v1,
|
||||
];
|
||||
|
||||
return useQuery(
|
||||
[cachingKeys, query],
|
||||
async () => {
|
||||
if (!isAssistantEnabled) {
|
||||
return {};
|
||||
}
|
||||
const res = await http.fetch<FetchConversationsResponse>(
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND,
|
||||
{
|
||||
method: 'GET',
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
version: API_VERSIONS.public.v1,
|
||||
query,
|
||||
signal,
|
||||
}
|
||||
|
|
|
@ -154,8 +154,6 @@ describe('API tests', () => {
|
|||
isEnabledRAGAlerts: true,
|
||||
assistantStreamingEnabled: false,
|
||||
alertsIndexPattern: '.alerts-security.alerts-default',
|
||||
allow: ['a', 'b', 'c'],
|
||||
allowReplacement: ['b', 'c'],
|
||||
replacements: { auuid: 'real.hostname' },
|
||||
size: 30,
|
||||
};
|
||||
|
@ -166,7 +164,7 @@ describe('API tests', () => {
|
|||
'/internal/elastic_assistant/actions/connector/foo/_execute',
|
||||
{
|
||||
...staticDefaults,
|
||||
body: '{"model":"gpt-4","message":"This is a test","subAction":"invokeAI","conversationId":"test","actionTypeId":".gen-ai","replacements":{"auuid":"real.hostname"},"isEnabledKnowledgeBase":true,"isEnabledRAGAlerts":true,"alertsIndexPattern":".alerts-security.alerts-default","allow":["a","b","c"],"allowReplacement":["b","c"],"size":30}',
|
||||
body: '{"model":"gpt-4","message":"This is a test","subAction":"invokeAI","conversationId":"test","actionTypeId":".gen-ai","replacements":{"auuid":"real.hostname"},"isEnabledKnowledgeBase":true,"isEnabledRAGAlerts":true,"alertsIndexPattern":".alerts-security.alerts-default","size":30}',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { ApiConfig, Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { API_VERSIONS, ApiConfig, Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { API_ERROR } from '../translations';
|
||||
import { getOptionalRequestParams } from '../helpers';
|
||||
import { TraceOptions } from '../types';
|
||||
|
@ -17,8 +17,6 @@ export interface FetchConnectorExecuteAction {
|
|||
conversationId: string;
|
||||
isEnabledRAGAlerts: boolean;
|
||||
alertsIndexPattern?: string;
|
||||
allow?: string[];
|
||||
allowReplacement?: string[];
|
||||
isEnabledKnowledgeBase: boolean;
|
||||
assistantStreamingEnabled: boolean;
|
||||
apiConfig: ApiConfig;
|
||||
|
@ -44,8 +42,6 @@ export const fetchConnectorExecuteAction = async ({
|
|||
conversationId,
|
||||
isEnabledRAGAlerts,
|
||||
alertsIndexPattern,
|
||||
allow,
|
||||
allowReplacement,
|
||||
isEnabledKnowledgeBase,
|
||||
assistantStreamingEnabled,
|
||||
http,
|
||||
|
@ -66,8 +62,6 @@ export const fetchConnectorExecuteAction = async ({
|
|||
const optionalRequestParams = getOptionalRequestParams({
|
||||
isEnabledRAGAlerts,
|
||||
alertsIndexPattern,
|
||||
allow,
|
||||
allowReplacement,
|
||||
size,
|
||||
});
|
||||
|
||||
|
@ -97,7 +91,7 @@ export const fetchConnectorExecuteAction = async ({
|
|||
signal,
|
||||
asResponse: true,
|
||||
rawResponse: true,
|
||||
version: '1',
|
||||
version: API_VERSIONS.internal.v1,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -132,7 +126,7 @@ export const fetchConnectorExecuteAction = async ({
|
|||
body: JSON.stringify(requestBody),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal,
|
||||
version: '1',
|
||||
version: API_VERSIONS.internal.v1,
|
||||
});
|
||||
|
||||
if (response.status !== 'ok' || !response.data) {
|
||||
|
@ -219,7 +213,7 @@ export const getKnowledgeBaseStatus = async ({
|
|||
const response = await http.fetch(path, {
|
||||
method: 'GET',
|
||||
signal,
|
||||
version: '1',
|
||||
version: API_VERSIONS.internal.v1,
|
||||
});
|
||||
|
||||
return response as GetKnowledgeBaseStatusResponse;
|
||||
|
@ -258,7 +252,7 @@ export const postKnowledgeBase = async ({
|
|||
const response = await http.fetch(path, {
|
||||
method: 'POST',
|
||||
signal,
|
||||
version: '1',
|
||||
version: API_VERSIONS.internal.v1,
|
||||
});
|
||||
|
||||
return response as PostKnowledgeBaseResponse;
|
||||
|
@ -297,7 +291,7 @@ export const deleteKnowledgeBase = async ({
|
|||
const response = await http.fetch(path, {
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
version: '1',
|
||||
version: API_VERSIONS.internal.v1,
|
||||
});
|
||||
|
||||
return response as DeleteKnowledgeBaseResponse;
|
||||
|
|
|
@ -37,6 +37,8 @@ const testProps = {
|
|||
showAnonymizedValues: false,
|
||||
conversations: mockConversations,
|
||||
refetchConversationsState: jest.fn(),
|
||||
anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
|
||||
refetchAnonymizationFieldsResults: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('../../connectorland/use_load_connectors', () => ({
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { DocLinksStart } from '@kbn/core-doc-links-browser';
|
||||
import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
|
||||
import { AIConnector } from '../../connectorland/connector_selector';
|
||||
import { Conversation } from '../../..';
|
||||
import { AssistantTitle } from '../assistant_title';
|
||||
|
@ -40,6 +41,8 @@ interface OwnProps {
|
|||
title: string | JSX.Element;
|
||||
conversations: Record<string, Conversation>;
|
||||
refetchConversationsState: () => Promise<void>;
|
||||
anonymizationFields: FindAnonymizationFieldsResponse;
|
||||
refetchAnonymizationFieldsResults: () => Promise<FindAnonymizationFieldsResponse | undefined>;
|
||||
}
|
||||
|
||||
type Props = OwnProps;
|
||||
|
@ -64,6 +67,8 @@ export const AssistantHeader: React.FC<Props> = ({
|
|||
setCurrentConversation,
|
||||
conversations,
|
||||
refetchConversationsState,
|
||||
anonymizationFields,
|
||||
refetchAnonymizationFieldsResults,
|
||||
}) => {
|
||||
const showAnonymizedValuesChecked = useMemo(
|
||||
() =>
|
||||
|
@ -147,6 +152,8 @@ export const AssistantHeader: React.FC<Props> = ({
|
|||
onConversationSelected={onConversationSelected}
|
||||
conversations={conversations}
|
||||
refetchConversationsState={refetchConversationsState}
|
||||
anonymizationFields={anonymizationFields}
|
||||
refetchAnonymizationFieldsResults={refetchAnonymizationFieldsResults}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -31,8 +31,7 @@ const mockPromptContexts: Record<string, PromptContext> = {
|
|||
};
|
||||
|
||||
const defaultProps = {
|
||||
defaultAllow: [],
|
||||
defaultAllowReplacement: [],
|
||||
anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
|
||||
promptContexts: mockPromptContexts,
|
||||
};
|
||||
|
||||
|
@ -79,8 +78,7 @@ describe('ContextPills', () => {
|
|||
it('it does NOT invoke setSelectedPromptContexts() when the prompt is already selected', async () => {
|
||||
const context = mockPromptContexts.context1;
|
||||
const mockSelectedPromptContext: SelectedPromptContext = {
|
||||
allow: [],
|
||||
allowReplacement: [],
|
||||
contextAnonymizationFields: { total: 0, page: 1, perPage: 100, data: [] },
|
||||
promptContextId: context.id,
|
||||
rawData: 'test-raw-data',
|
||||
};
|
||||
|
@ -109,8 +107,7 @@ describe('ContextPills', () => {
|
|||
it('disables selected context pills', () => {
|
||||
const context = mockPromptContexts.context1;
|
||||
const mockSelectedPromptContext: SelectedPromptContext = {
|
||||
allow: [],
|
||||
allowReplacement: [],
|
||||
contextAnonymizationFields: { total: 0, page: 1, perPage: 100, data: [] },
|
||||
promptContextId: context.id,
|
||||
rawData: 'test-raw-data',
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import React, { useCallback, useMemo } from 'react';
|
|||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
|
||||
import { getNewSelectedPromptContext } from '../../data_anonymization/get_new_selected_prompt_context';
|
||||
import type { PromptContext, SelectedPromptContext } from '../prompt_context/types';
|
||||
|
||||
|
@ -19,8 +20,7 @@ const PillButton = styled(EuiButton)`
|
|||
`;
|
||||
|
||||
interface Props {
|
||||
defaultAllow: string[];
|
||||
defaultAllowReplacement: string[];
|
||||
anonymizationFields: FindAnonymizationFieldsResponse;
|
||||
promptContexts: Record<string, PromptContext>;
|
||||
selectedPromptContexts: Record<string, SelectedPromptContext>;
|
||||
setSelectedPromptContexts: React.Dispatch<
|
||||
|
@ -29,8 +29,7 @@ interface Props {
|
|||
}
|
||||
|
||||
const ContextPillsComponent: React.FC<Props> = ({
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
anonymizationFields,
|
||||
promptContexts,
|
||||
selectedPromptContexts,
|
||||
setSelectedPromptContexts,
|
||||
|
@ -44,8 +43,7 @@ const ContextPillsComponent: React.FC<Props> = ({
|
|||
async (id: string) => {
|
||||
if (selectedPromptContexts[id] == null && promptContexts[id] != null) {
|
||||
const newSelectedPromptContext = await getNewSelectedPromptContext({
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
anonymizationFields,
|
||||
promptContext: promptContexts[id],
|
||||
});
|
||||
|
||||
|
@ -55,13 +53,7 @@ const ContextPillsComponent: React.FC<Props> = ({
|
|||
}));
|
||||
}
|
||||
},
|
||||
[
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
promptContexts,
|
||||
selectedPromptContexts,
|
||||
setSelectedPromptContexts,
|
||||
]
|
||||
[anonymizationFields, promptContexts, selectedPromptContexts, setSelectedPromptContexts]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -167,8 +167,6 @@ describe('helpers', () => {
|
|||
const params = {
|
||||
isEnabledRAGAlerts: false, // <-- false
|
||||
alertsIndexPattern: 'indexPattern',
|
||||
allow: ['a', 'b', 'c'],
|
||||
allowReplacement: ['b', 'c'],
|
||||
size: 10,
|
||||
};
|
||||
|
||||
|
@ -181,8 +179,6 @@ describe('helpers', () => {
|
|||
const params = {
|
||||
isEnabledRAGAlerts: true,
|
||||
alertsIndexPattern: 'indexPattern',
|
||||
allow: ['a', 'b', 'c'],
|
||||
allowReplacement: ['b', 'c'],
|
||||
size: 10,
|
||||
};
|
||||
|
||||
|
@ -190,24 +186,9 @@ describe('helpers', () => {
|
|||
|
||||
expect(result).toEqual({
|
||||
alertsIndexPattern: 'indexPattern',
|
||||
allow: ['a', 'b', 'c'],
|
||||
allowReplacement: ['b', 'c'],
|
||||
size: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return (only) the optional request params that are defined when some optional params are not provided', () => {
|
||||
const params = {
|
||||
isEnabledRAGAlerts: true,
|
||||
allow: ['a', 'b', 'c'], // all the others are undefined
|
||||
};
|
||||
|
||||
const result = getOptionalRequestParams(params);
|
||||
|
||||
expect(result).toEqual({
|
||||
allow: ['a', 'b', 'c'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeBaseWithPersistedConversations', () => {
|
||||
|
|
|
@ -85,27 +85,19 @@ export const getDefaultConnector = (
|
|||
|
||||
interface OptionalRequestParams {
|
||||
alertsIndexPattern?: string;
|
||||
allow?: string[];
|
||||
allowReplacement?: string[];
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export const getOptionalRequestParams = ({
|
||||
isEnabledRAGAlerts,
|
||||
alertsIndexPattern,
|
||||
allow,
|
||||
allowReplacement,
|
||||
size,
|
||||
}: {
|
||||
isEnabledRAGAlerts: boolean;
|
||||
alertsIndexPattern?: string;
|
||||
allow?: string[];
|
||||
allowReplacement?: string[];
|
||||
size?: number;
|
||||
}): OptionalRequestParams => {
|
||||
const optionalAlertsIndexPattern = alertsIndexPattern ? { alertsIndexPattern } : undefined;
|
||||
const optionalAllow = allow ? { allow } : undefined;
|
||||
const optionalAllowReplacement = allowReplacement ? { allowReplacement } : undefined;
|
||||
const optionalSize = size ? { size } : undefined;
|
||||
|
||||
// the settings toggle must be enabled:
|
||||
|
@ -115,8 +107,6 @@ export const getOptionalRequestParams = ({
|
|||
|
||||
return {
|
||||
...optionalAlertsIndexPattern,
|
||||
...optionalAllow,
|
||||
...optionalAllowReplacement,
|
||||
...optionalSize,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
import { createPortal } from 'react-dom';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
|
||||
import { useChatSend } from './chat_send/use_chat_send';
|
||||
import { ChatSend } from './chat_send';
|
||||
import { BlockBotCallToAction } from './block_bot/cta';
|
||||
|
@ -57,6 +58,7 @@ import {
|
|||
} from './api/conversations/use_fetch_current_user_conversations';
|
||||
import { Conversation } from '../assistant_context/types';
|
||||
import { clearPresentationData } from '../connectorland/connector_setup/helpers';
|
||||
import { useFetchAnonymizationFields } from './api/anonymization_fields/use_fetch_anonymization_fields';
|
||||
|
||||
export interface Props {
|
||||
conversationTitle?: string;
|
||||
|
@ -83,8 +85,6 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
assistantTelemetry,
|
||||
augmentMessageCodeBlocks,
|
||||
assistantAvailability: { isAssistantEnabled },
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
docLinks,
|
||||
getComments,
|
||||
http,
|
||||
|
@ -124,8 +124,36 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
http,
|
||||
onFetch: onFetchedConversations,
|
||||
refetchOnWindowFocus: !isStreaming,
|
||||
isAssistantEnabled,
|
||||
});
|
||||
|
||||
const [anonymizationFields, setAnonymizationFields] = useState<FindAnonymizationFieldsResponse>({
|
||||
data: [],
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
total: 0,
|
||||
});
|
||||
const {
|
||||
data: anonymizationData,
|
||||
isLoading: isLoadingAnonymizationFields,
|
||||
isError: isErrorAnonymizationFields,
|
||||
refetch: refetchAnonymizationFields,
|
||||
} = useFetchAnonymizationFields({ http, isAssistantEnabled });
|
||||
|
||||
const refetchAnonymizationFieldsResults = useCallback(async () => {
|
||||
const updatedAnonymizationFields = await refetchAnonymizationFields();
|
||||
if (!updatedAnonymizationFields.isLoading && !updatedAnonymizationFields.isError) {
|
||||
setAnonymizationFields(updatedAnonymizationFields.data);
|
||||
return updatedAnonymizationFields.data as FindAnonymizationFieldsResponse;
|
||||
}
|
||||
}, [refetchAnonymizationFields]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingAnonymizationFields && !isErrorAnonymizationFields) {
|
||||
setAnonymizationFields(anonymizationData);
|
||||
}
|
||||
}, [anonymizationData, isErrorAnonymizationFields, isLoadingAnonymizationFields]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isError) {
|
||||
setConversations(conversationsData ?? {});
|
||||
|
@ -382,14 +410,18 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
}
|
||||
|
||||
const promptContext: PromptContext | undefined = promptContexts[promptContextId];
|
||||
if (promptContext != null) {
|
||||
if (
|
||||
promptContext != null &&
|
||||
!isLoadingAnonymizationFields &&
|
||||
!isErrorAnonymizationFields &&
|
||||
anonymizationData
|
||||
) {
|
||||
setAutoPopulatedOnce(true);
|
||||
|
||||
if (!Object.keys(selectedPromptContexts).includes(promptContext.id)) {
|
||||
const addNewSelectedPromptContext = async () => {
|
||||
const newSelectedPromptContext = await getNewSelectedPromptContext({
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
anonymizationFields: anonymizationData,
|
||||
promptContext,
|
||||
});
|
||||
|
||||
|
@ -414,8 +446,9 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
selectedConversationTitle,
|
||||
selectedPromptContexts,
|
||||
autoPopulatedOnce,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
isLoadingAnonymizationFields,
|
||||
isErrorAnonymizationFields,
|
||||
anonymizationData,
|
||||
]);
|
||||
|
||||
// Show missing connector callout if no connectors are configured
|
||||
|
@ -583,17 +616,18 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
conversations={conversations}
|
||||
onConversationDeleted={handleOnConversationDeleted}
|
||||
refetchConversationsState={refetchConversationsState}
|
||||
anonymizationFields={anonymizationFields}
|
||||
refetchAnonymizationFieldsResults={refetchAnonymizationFieldsResults}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Create portals for each EuiCodeBlock to add the `Investigate in Timeline` action */}
|
||||
{createCodeBlockPortals()}
|
||||
|
||||
{!isDisabled && (
|
||||
{!isDisabled && !isLoadingAnonymizationFields && !isErrorAnonymizationFields && (
|
||||
<>
|
||||
<ContextPills
|
||||
defaultAllow={defaultAllow}
|
||||
defaultAllowReplacement={defaultAllowReplacement}
|
||||
anonymizationFields={anonymizationData}
|
||||
promptContexts={promptContexts}
|
||||
selectedPromptContexts={selectedPromptContexts}
|
||||
setSelectedPromptContexts={setSelectedPromptContexts}
|
||||
|
|
|
@ -13,8 +13,7 @@ import { mockAlertPromptContext } from '../../mock/prompt_context';
|
|||
import type { SelectedPromptContext } from '../prompt_context/types';
|
||||
|
||||
const mockSelectedAlertPromptContext: SelectedPromptContext = {
|
||||
allow: [],
|
||||
allowReplacement: [],
|
||||
contextAnonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
|
||||
promptContextId: mockAlertPromptContext.id,
|
||||
rawData: 'alert data',
|
||||
};
|
||||
|
@ -152,8 +151,25 @@ User prompt text`);
|
|||
|
||||
describe('when there is data to anonymize', () => {
|
||||
const mockPromptContextWithDataToAnonymize: SelectedPromptContext = {
|
||||
allow: ['field1', 'field2'],
|
||||
allowReplacement: ['field1', 'field2'],
|
||||
contextAnonymizationFields: {
|
||||
total: 0,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{
|
||||
id: 'field1',
|
||||
field: 'field1',
|
||||
anonymized: true,
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
id: 'field2',
|
||||
field: 'field2',
|
||||
anonymized: true,
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
promptContextId: 'test-prompt-context-id',
|
||||
rawData: {
|
||||
field1: ['foo', 'bar', 'baz'],
|
||||
|
|
|
@ -64,8 +64,7 @@ export function getCombinedMessage({
|
|||
.sort()
|
||||
.map((id) => {
|
||||
const promptContextData = transformRawData({
|
||||
allow: selectedPromptContexts[id].allow,
|
||||
allowReplacement: selectedPromptContexts[id].allowReplacement,
|
||||
anonymizationFields: selectedPromptContexts[id].contextAnonymizationFields?.data ?? [],
|
||||
currentReplacements,
|
||||
getAnonymizedValue,
|
||||
onNewReplacements,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
/**
|
||||
|
@ -68,10 +69,8 @@ export interface PromptContext {
|
|||
* A prompt context that was added from the pills to the current conversation, but not yet sent
|
||||
*/
|
||||
export interface SelectedPromptContext {
|
||||
/** fields allowed to be included in a conversation */
|
||||
allow: string[];
|
||||
/** fields that will be anonymized */
|
||||
allowReplacement: string[];
|
||||
/** anonymization fields to be included in a conversation */
|
||||
contextAnonymizationFields?: FindAnonymizationFieldsResponse;
|
||||
/** unique id of the selected `PromptContext` */
|
||||
promptContextId: string;
|
||||
/** this data is not anonymized */
|
||||
|
|
|
@ -14,15 +14,13 @@ import { SelectedPromptContext } from '../prompt_context/types';
|
|||
import { PromptEditor, Props } from '.';
|
||||
|
||||
const mockSelectedAlertPromptContext: SelectedPromptContext = {
|
||||
allow: [],
|
||||
allowReplacement: [],
|
||||
contextAnonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
|
||||
promptContextId: mockAlertPromptContext.id,
|
||||
rawData: 'alert data',
|
||||
};
|
||||
|
||||
const mockSelectedEventPromptContext: SelectedPromptContext = {
|
||||
allow: [],
|
||||
allowReplacement: [],
|
||||
contextAnonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
|
||||
promptContextId: mockEventPromptContext.id,
|
||||
rawData: 'event data',
|
||||
};
|
||||
|
|
|
@ -25,15 +25,13 @@ const defaultProps: Props = {
|
|||
};
|
||||
|
||||
const mockSelectedAlertPromptContext: SelectedPromptContext = {
|
||||
allow: [],
|
||||
allowReplacement: [],
|
||||
contextAnonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
|
||||
promptContextId: mockAlertPromptContext.id,
|
||||
rawData: 'test-raw-data',
|
||||
};
|
||||
|
||||
const mockSelectedEventPromptContext: SelectedPromptContext = {
|
||||
allow: [],
|
||||
allowReplacement: [],
|
||||
contextAnonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
|
||||
promptContextId: mockEventPromptContext.id,
|
||||
rawData: 'test-raw-data',
|
||||
};
|
||||
|
|
|
@ -52,6 +52,8 @@ const testProps = {
|
|||
onSave,
|
||||
onConversationSelected,
|
||||
conversations: {},
|
||||
anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
|
||||
refetchAnonymizationFieldsResults: jest.fn(),
|
||||
};
|
||||
jest.mock('../../assistant_context');
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import styled from 'styled-components';
|
||||
import { css } from '@emotion/react';
|
||||
import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
|
||||
import { AIConnector } from '../../connectorland/connector_selector';
|
||||
import { Conversation, Prompt, QuickPrompt } from '../../..';
|
||||
import * as i18n from './translations';
|
||||
|
@ -66,6 +67,8 @@ interface Props {
|
|||
selectedConversation: Conversation;
|
||||
onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void;
|
||||
conversations: Record<string, Conversation>;
|
||||
anonymizationFields: FindAnonymizationFieldsResponse;
|
||||
refetchAnonymizationFieldsResults: () => Promise<FindAnonymizationFieldsResponse | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -80,6 +83,8 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
selectedConversation: defaultSelectedConversation,
|
||||
onConversationSelected,
|
||||
conversations,
|
||||
anonymizationFields,
|
||||
refetchAnonymizationFieldsResults,
|
||||
}) => {
|
||||
const {
|
||||
actionTypeRegistry,
|
||||
|
@ -92,22 +97,22 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
const {
|
||||
conversationSettings,
|
||||
setConversationSettings,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
knowledgeBase,
|
||||
quickPromptSettings,
|
||||
systemPromptSettings,
|
||||
assistantStreamingEnabled,
|
||||
setUpdatedAssistantStreamingEnabled,
|
||||
setUpdatedDefaultAllow,
|
||||
setUpdatedDefaultAllowReplacement,
|
||||
setUpdatedKnowledgeBaseSettings,
|
||||
setUpdatedQuickPromptSettings,
|
||||
setUpdatedSystemPromptSettings,
|
||||
saveSettings,
|
||||
conversationsSettingsBulkActions,
|
||||
updatedAnonymizationData,
|
||||
setConversationsSettingsBulkActions,
|
||||
} = useSettingsUpdater(conversations);
|
||||
anonymizationFieldsBulkActions,
|
||||
setAnonymizationFieldsBulkActions,
|
||||
setUpdatedAnonymizationData,
|
||||
} = useSettingsUpdater(conversations, anonymizationFields);
|
||||
|
||||
// Local state for saving previously selected items so tab switching is friendlier
|
||||
// Conversation Selection State
|
||||
|
@ -162,12 +167,21 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
});
|
||||
}
|
||||
const saveResult = await saveSettings();
|
||||
await onSave(saveResult);
|
||||
if (
|
||||
(anonymizationFieldsBulkActions?.create?.length ?? 0) > 0 ||
|
||||
(anonymizationFieldsBulkActions?.update?.length ?? 0) > 0 ||
|
||||
(anonymizationFieldsBulkActions?.delete?.ids?.length ?? 0) > 0
|
||||
) {
|
||||
refetchAnonymizationFieldsResults();
|
||||
}
|
||||
onSave(saveResult);
|
||||
}, [
|
||||
anonymizationFieldsBulkActions,
|
||||
conversationSettings,
|
||||
defaultSelectedConversation.title,
|
||||
onConversationSelected,
|
||||
onSave,
|
||||
refetchAnonymizationFieldsResults,
|
||||
saveSettings,
|
||||
]);
|
||||
|
||||
|
@ -330,11 +344,11 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
)}
|
||||
{selectedSettingsTab === ANONYMIZATION_TAB && (
|
||||
<AnonymizationSettings
|
||||
defaultAllow={defaultAllow}
|
||||
defaultAllowReplacement={defaultAllowReplacement}
|
||||
pageSize={5}
|
||||
setUpdatedDefaultAllow={setUpdatedDefaultAllow}
|
||||
setUpdatedDefaultAllowReplacement={setUpdatedDefaultAllowReplacement}
|
||||
defaultPageSize={5}
|
||||
anonymizationFields={updatedAnonymizationData}
|
||||
anonymizationFieldsBulkActions={anonymizationFieldsBulkActions}
|
||||
setAnonymizationFieldsBulkActions={setAnonymizationFieldsBulkActions}
|
||||
setUpdatedAnonymizationData={setUpdatedAnonymizationData}
|
||||
/>
|
||||
)}
|
||||
{selectedSettingsTab === KNOWLEDGE_BASE_TAB && (
|
||||
|
|
|
@ -25,6 +25,8 @@ const testProps = {
|
|||
onConversationSelected,
|
||||
conversations: {},
|
||||
refetchConversationsState: jest.fn(),
|
||||
anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] },
|
||||
refetchAnonymizationFieldsResults: jest.fn(),
|
||||
};
|
||||
const setSelectedSettingsTab = jest.fn();
|
||||
const mockUseAssistantContext = {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
|
||||
import { AIConnector } from '../../connectorland/connector_selector';
|
||||
import { Conversation } from '../../..';
|
||||
import { AssistantSettings, CONVERSATIONS_TAB } from './assistant_settings';
|
||||
|
@ -23,6 +24,8 @@ interface Props {
|
|||
isDisabled?: boolean;
|
||||
conversations: Record<string, Conversation>;
|
||||
refetchConversationsState: () => Promise<void>;
|
||||
anonymizationFields: FindAnonymizationFieldsResponse;
|
||||
refetchAnonymizationFieldsResults: () => Promise<FindAnonymizationFieldsResponse | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,6 +41,8 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
|
|||
onConversationSelected,
|
||||
conversations,
|
||||
refetchConversationsState,
|
||||
anonymizationFields,
|
||||
refetchAnonymizationFieldsResults,
|
||||
}) => {
|
||||
const { toasts, setSelectedSettingsTab } = useAssistantContext();
|
||||
|
||||
|
@ -90,6 +95,8 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
|
|||
onClose={handleCloseModal}
|
||||
onSave={handleSave}
|
||||
conversations={conversations}
|
||||
anonymizationFields={anonymizationFields}
|
||||
refetchAnonymizationFieldsResults={refetchAnonymizationFieldsResults}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -29,24 +29,28 @@ const mockHttp = {
|
|||
const mockSystemPrompts: Prompt[] = [mockSystemPrompt];
|
||||
const mockQuickPrompts: Prompt[] = [defaultSystemPrompt];
|
||||
|
||||
const initialDefaultAllow = ['allow1'];
|
||||
const initialDefaultAllowReplacement = ['replacement1'];
|
||||
const anonymizationFields = {
|
||||
total: 2,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{ id: 'allow1', field: 'allow1', allowed: true, anonymized: false },
|
||||
{ id: 'replacement1', field: 'replacement1', allowed: false, anonymized: true },
|
||||
],
|
||||
};
|
||||
|
||||
const setAllQuickPromptsMock = jest.fn();
|
||||
const setAllSystemPromptsMock = jest.fn();
|
||||
const setDefaultAllowMock = jest.fn();
|
||||
const setAssistantStreamingEnabled = jest.fn();
|
||||
const setDefaultAllowReplacementMock = jest.fn();
|
||||
const setKnowledgeBaseMock = jest.fn();
|
||||
const reportAssistantSettingToggled = jest.fn();
|
||||
const setUpdatedAnonymizationData = jest.fn();
|
||||
const mockValues = {
|
||||
assistantStreamingEnabled: true,
|
||||
setAssistantStreamingEnabled,
|
||||
assistantTelemetry: { reportAssistantSettingToggled },
|
||||
allSystemPrompts: mockSystemPrompts,
|
||||
allQuickPrompts: mockQuickPrompts,
|
||||
defaultAllow: initialDefaultAllow,
|
||||
defaultAllowReplacement: initialDefaultAllowReplacement,
|
||||
knowledgeBase: {
|
||||
isEnabledRAGAlerts: true,
|
||||
isEnabledKnowledgeBase: true,
|
||||
|
@ -55,18 +59,24 @@ const mockValues = {
|
|||
baseConversations: {},
|
||||
setAllQuickPrompts: setAllQuickPromptsMock,
|
||||
setAllSystemPrompts: setAllSystemPromptsMock,
|
||||
setDefaultAllow: setDefaultAllowMock,
|
||||
setDefaultAllowReplacement: setDefaultAllowReplacementMock,
|
||||
setKnowledgeBase: setKnowledgeBaseMock,
|
||||
http: mockHttp,
|
||||
anonymizationFieldsBulkActions: {},
|
||||
};
|
||||
|
||||
const updatedValues = {
|
||||
conversations: { [customConvo.title]: customConvo },
|
||||
allSystemPrompts: [mockSuperheroSystemPrompt],
|
||||
allQuickPrompts: [{ title: 'Prompt 2', prompt: 'Prompt 2', color: 'red' }],
|
||||
defaultAllow: ['allow2'],
|
||||
defaultAllowReplacement: ['replacement2'],
|
||||
updatedAnonymizationData: {
|
||||
total: 2,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{ id: 'allow2', field: 'allow2', allowed: true, anonymized: false },
|
||||
{ id: 'replacement2', field: 'replacement2', allowed: false, anonymized: true },
|
||||
],
|
||||
},
|
||||
knowledgeBase: {
|
||||
isEnabledRAGAlerts: false,
|
||||
isEnabledKnowledgeBase: false,
|
||||
|
@ -89,15 +99,15 @@ describe('useSettingsUpdater', () => {
|
|||
});
|
||||
it('should set all state variables to their initial values when resetSettings is called', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater(mockConversations));
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useSettingsUpdater(mockConversations, anonymizationFields)
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
const {
|
||||
setConversationSettings,
|
||||
setConversationsSettingsBulkActions,
|
||||
setUpdatedQuickPromptSettings,
|
||||
setUpdatedSystemPromptSettings,
|
||||
setUpdatedDefaultAllow,
|
||||
setUpdatedDefaultAllowReplacement,
|
||||
setUpdatedKnowledgeBaseSettings,
|
||||
setUpdatedAssistantStreamingEnabled,
|
||||
resetSettings,
|
||||
|
@ -107,16 +117,14 @@ describe('useSettingsUpdater', () => {
|
|||
setConversationsSettingsBulkActions({});
|
||||
setUpdatedQuickPromptSettings(updatedValues.allQuickPrompts);
|
||||
setUpdatedSystemPromptSettings(updatedValues.allSystemPrompts);
|
||||
setUpdatedDefaultAllow(updatedValues.defaultAllow);
|
||||
setUpdatedDefaultAllowReplacement(updatedValues.defaultAllowReplacement);
|
||||
setUpdatedAnonymizationData(updatedValues.updatedAnonymizationData);
|
||||
setUpdatedKnowledgeBaseSettings(updatedValues.knowledgeBase);
|
||||
setUpdatedAssistantStreamingEnabled(updatedValues.assistantStreamingEnabled);
|
||||
|
||||
expect(result.current.conversationSettings).toEqual(updatedValues.conversations);
|
||||
expect(result.current.quickPromptSettings).toEqual(updatedValues.allQuickPrompts);
|
||||
expect(result.current.systemPromptSettings).toEqual(updatedValues.allSystemPrompts);
|
||||
expect(result.current.defaultAllow).toEqual(updatedValues.defaultAllow);
|
||||
expect(result.current.defaultAllowReplacement).toEqual(updatedValues.defaultAllowReplacement);
|
||||
expect(result.current.updatedAnonymizationData).toEqual(anonymizationFields);
|
||||
expect(result.current.knowledgeBase).toEqual(updatedValues.knowledgeBase);
|
||||
expect(result.current.assistantStreamingEnabled).toEqual(
|
||||
updatedValues.assistantStreamingEnabled
|
||||
|
@ -127,8 +135,9 @@ describe('useSettingsUpdater', () => {
|
|||
expect(result.current.conversationSettings).toEqual(mockConversations);
|
||||
expect(result.current.quickPromptSettings).toEqual(mockValues.allQuickPrompts);
|
||||
expect(result.current.systemPromptSettings).toEqual(mockValues.allSystemPrompts);
|
||||
expect(result.current.defaultAllow).toEqual(mockValues.defaultAllow);
|
||||
expect(result.current.defaultAllowReplacement).toEqual(mockValues.defaultAllowReplacement);
|
||||
expect(result.current.anonymizationFieldsBulkActions).toEqual(
|
||||
mockValues.anonymizationFieldsBulkActions
|
||||
);
|
||||
expect(result.current.knowledgeBase).toEqual(mockValues.knowledgeBase);
|
||||
expect(result.current.assistantStreamingEnabled).toEqual(
|
||||
mockValues.assistantStreamingEnabled
|
||||
|
@ -138,24 +147,25 @@ describe('useSettingsUpdater', () => {
|
|||
|
||||
it('should update all state variables to their updated values when saveSettings is called', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater(mockConversations));
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useSettingsUpdater(mockConversations, anonymizationFields)
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
const {
|
||||
setConversationSettings,
|
||||
setConversationsSettingsBulkActions,
|
||||
setUpdatedQuickPromptSettings,
|
||||
setUpdatedSystemPromptSettings,
|
||||
setUpdatedDefaultAllow,
|
||||
setUpdatedDefaultAllowReplacement,
|
||||
setAnonymizationFieldsBulkActions,
|
||||
setUpdatedKnowledgeBaseSettings,
|
||||
} = result.current;
|
||||
|
||||
setConversationSettings(updatedValues.conversations);
|
||||
setConversationsSettingsBulkActions({ delete: { ids: ['1'] } });
|
||||
setAnonymizationFieldsBulkActions({ delete: { ids: ['1'] } });
|
||||
setUpdatedQuickPromptSettings(updatedValues.allQuickPrompts);
|
||||
setUpdatedSystemPromptSettings(updatedValues.allSystemPrompts);
|
||||
setUpdatedDefaultAllow(updatedValues.defaultAllow);
|
||||
setUpdatedDefaultAllowReplacement(updatedValues.defaultAllowReplacement);
|
||||
setUpdatedAnonymizationData(updatedValues.updatedAnonymizationData);
|
||||
setUpdatedKnowledgeBaseSettings(updatedValues.knowledgeBase);
|
||||
|
||||
await result.current.saveSettings();
|
||||
|
@ -170,16 +180,17 @@ describe('useSettingsUpdater', () => {
|
|||
);
|
||||
expect(setAllQuickPromptsMock).toHaveBeenCalledWith(updatedValues.allQuickPrompts);
|
||||
expect(setAllSystemPromptsMock).toHaveBeenCalledWith(updatedValues.allSystemPrompts);
|
||||
expect(setDefaultAllowMock).toHaveBeenCalledWith(updatedValues.defaultAllow);
|
||||
expect(setDefaultAllowReplacementMock).toHaveBeenCalledWith(
|
||||
updatedValues.defaultAllowReplacement
|
||||
expect(setUpdatedAnonymizationData).toHaveBeenCalledWith(
|
||||
updatedValues.updatedAnonymizationData
|
||||
);
|
||||
expect(setKnowledgeBaseMock).toHaveBeenCalledWith(updatedValues.knowledgeBase);
|
||||
});
|
||||
});
|
||||
it('should track which toggles have been updated when saveSettings is called', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater(mockConversations));
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useSettingsUpdater(mockConversations, anonymizationFields)
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
const { setUpdatedKnowledgeBaseSettings } = result.current;
|
||||
|
||||
|
@ -194,7 +205,9 @@ describe('useSettingsUpdater', () => {
|
|||
});
|
||||
it('should track only toggles that updated', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater(mockConversations));
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useSettingsUpdater(mockConversations, anonymizationFields)
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
const { setUpdatedKnowledgeBaseSettings } = result.current;
|
||||
|
||||
|
@ -210,7 +223,9 @@ describe('useSettingsUpdater', () => {
|
|||
});
|
||||
it('if no toggles update, do not track anything', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater(mockConversations));
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useSettingsUpdater(mockConversations, anonymizationFields)
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
const { setUpdatedKnowledgeBaseSettings } = result.current;
|
||||
|
||||
|
|
|
@ -6,30 +6,37 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
|
||||
import { PerformBulkActionRequestBody } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { Conversation, Prompt, QuickPrompt } from '../../../..';
|
||||
import { useAssistantContext } from '../../../assistant_context';
|
||||
import type { KnowledgeBaseConfig } from '../../types';
|
||||
import {
|
||||
ConversationsBulkActions,
|
||||
bulkChangeConversations,
|
||||
} from '../../api/conversations/use_bulk_actions_conversations';
|
||||
bulkUpdateConversations,
|
||||
} from '../../api/conversations/bulk_update_actions_conversations';
|
||||
import { bulkUpdateAnonymizationFields } from '../../api/anonymization_fields/bulk_update_anonymization_fields';
|
||||
|
||||
interface UseSettingsUpdater {
|
||||
assistantStreamingEnabled: boolean;
|
||||
conversationSettings: Record<string, Conversation>;
|
||||
conversationsSettingsBulkActions: ConversationsBulkActions;
|
||||
defaultAllow: string[];
|
||||
defaultAllowReplacement: string[];
|
||||
updatedAnonymizationData: FindAnonymizationFieldsResponse;
|
||||
knowledgeBase: KnowledgeBaseConfig;
|
||||
quickPromptSettings: QuickPrompt[];
|
||||
resetSettings: () => void;
|
||||
systemPromptSettings: Prompt[];
|
||||
setUpdatedDefaultAllow: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setUpdatedDefaultAllowReplacement: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setUpdatedAnonymizationData: React.Dispatch<
|
||||
React.SetStateAction<FindAnonymizationFieldsResponse>
|
||||
>;
|
||||
setConversationSettings: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
|
||||
setConversationsSettingsBulkActions: React.Dispatch<
|
||||
React.SetStateAction<ConversationsBulkActions>
|
||||
>;
|
||||
anonymizationFieldsBulkActions: PerformBulkActionRequestBody;
|
||||
setAnonymizationFieldsBulkActions: React.Dispatch<
|
||||
React.SetStateAction<PerformBulkActionRequestBody>
|
||||
>;
|
||||
setUpdatedKnowledgeBaseSettings: React.Dispatch<React.SetStateAction<KnowledgeBaseConfig>>;
|
||||
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<QuickPrompt[]>>;
|
||||
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<Prompt[]>>;
|
||||
|
@ -38,20 +45,17 @@ interface UseSettingsUpdater {
|
|||
}
|
||||
|
||||
export const useSettingsUpdater = (
|
||||
conversations: Record<string, Conversation>
|
||||
conversations: Record<string, Conversation>,
|
||||
anonymizationFields: FindAnonymizationFieldsResponse
|
||||
): UseSettingsUpdater => {
|
||||
// Initial state from assistant context
|
||||
const {
|
||||
allQuickPrompts,
|
||||
allSystemPrompts,
|
||||
assistantTelemetry,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
knowledgeBase,
|
||||
setAllQuickPrompts,
|
||||
setAllSystemPrompts,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
assistantStreamingEnabled,
|
||||
setAssistantStreamingEnabled,
|
||||
setKnowledgeBase,
|
||||
|
@ -74,9 +78,10 @@ export const useSettingsUpdater = (
|
|||
const [updatedSystemPromptSettings, setUpdatedSystemPromptSettings] =
|
||||
useState<Prompt[]>(allSystemPrompts);
|
||||
// Anonymization
|
||||
const [updatedDefaultAllow, setUpdatedDefaultAllow] = useState<string[]>(defaultAllow);
|
||||
const [updatedDefaultAllowReplacement, setUpdatedDefaultAllowReplacement] =
|
||||
useState<string[]>(defaultAllowReplacement);
|
||||
const [anonymizationFieldsBulkActions, setAnonymizationFieldsBulkActions] =
|
||||
useState<PerformBulkActionRequestBody>({});
|
||||
const [updatedAnonymizationData, setUpdatedAnonymizationData] =
|
||||
useState<FindAnonymizationFieldsResponse>(anonymizationFields);
|
||||
const [updatedAssistantStreamingEnabled, setUpdatedAssistantStreamingEnabled] =
|
||||
useState<boolean>(assistantStreamingEnabled);
|
||||
// Knowledge Base
|
||||
|
@ -93,15 +98,13 @@ export const useSettingsUpdater = (
|
|||
setUpdatedKnowledgeBaseSettings(knowledgeBase);
|
||||
setUpdatedAssistantStreamingEnabled(assistantStreamingEnabled);
|
||||
setUpdatedSystemPromptSettings(allSystemPrompts);
|
||||
setUpdatedDefaultAllow(defaultAllow);
|
||||
setUpdatedDefaultAllowReplacement(defaultAllowReplacement);
|
||||
setUpdatedAnonymizationData(anonymizationFields);
|
||||
}, [
|
||||
allQuickPrompts,
|
||||
allSystemPrompts,
|
||||
anonymizationFields,
|
||||
assistantStreamingEnabled,
|
||||
conversations,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
knowledgeBase,
|
||||
]);
|
||||
|
||||
|
@ -117,7 +120,7 @@ export const useSettingsUpdater = (
|
|||
conversationsSettingsBulkActions.update ||
|
||||
conversationsSettingsBulkActions.delete;
|
||||
const bulkResult = hasBulkConversations
|
||||
? await bulkChangeConversations(http, conversationsSettingsBulkActions, toasts)
|
||||
? await bulkUpdateConversations(http, conversationsSettingsBulkActions, toasts)
|
||||
: undefined;
|
||||
|
||||
const didUpdateKnowledgeBase =
|
||||
|
@ -141,17 +144,22 @@ export const useSettingsUpdater = (
|
|||
}
|
||||
setAssistantStreamingEnabled(updatedAssistantStreamingEnabled);
|
||||
setKnowledgeBase(updatedKnowledgeBaseSettings);
|
||||
setDefaultAllow(updatedDefaultAllow);
|
||||
setDefaultAllowReplacement(updatedDefaultAllowReplacement);
|
||||
const hasBulkAnonymizationFields =
|
||||
anonymizationFieldsBulkActions.create ||
|
||||
anonymizationFieldsBulkActions.update ||
|
||||
anonymizationFieldsBulkActions.delete;
|
||||
const bulkAnonymizationFieldsResult = hasBulkAnonymizationFields
|
||||
? await bulkUpdateAnonymizationFields(http, anonymizationFieldsBulkActions, toasts)
|
||||
: undefined;
|
||||
|
||||
return bulkResult?.success ?? true;
|
||||
return (bulkResult?.success ?? true) && (bulkAnonymizationFieldsResult?.success ?? true);
|
||||
}, [
|
||||
setAllQuickPrompts,
|
||||
updatedQuickPromptSettings,
|
||||
setAllSystemPrompts,
|
||||
updatedSystemPromptSettings,
|
||||
http,
|
||||
conversationsSettingsBulkActions,
|
||||
http,
|
||||
toasts,
|
||||
knowledgeBase.isEnabledKnowledgeBase,
|
||||
knowledgeBase.isEnabledRAGAlerts,
|
||||
|
@ -160,26 +168,23 @@ export const useSettingsUpdater = (
|
|||
assistantStreamingEnabled,
|
||||
setAssistantStreamingEnabled,
|
||||
setKnowledgeBase,
|
||||
setDefaultAllow,
|
||||
updatedDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
updatedDefaultAllowReplacement,
|
||||
anonymizationFieldsBulkActions,
|
||||
assistantTelemetry,
|
||||
]);
|
||||
|
||||
return {
|
||||
conversationSettings,
|
||||
conversationsSettingsBulkActions,
|
||||
defaultAllow: updatedDefaultAllow,
|
||||
defaultAllowReplacement: updatedDefaultAllowReplacement,
|
||||
knowledgeBase: updatedKnowledgeBaseSettings,
|
||||
assistantStreamingEnabled: updatedAssistantStreamingEnabled,
|
||||
quickPromptSettings: updatedQuickPromptSettings,
|
||||
resetSettings,
|
||||
systemPromptSettings: updatedSystemPromptSettings,
|
||||
saveSettings,
|
||||
setUpdatedDefaultAllow,
|
||||
setUpdatedDefaultAllowReplacement,
|
||||
updatedAnonymizationData,
|
||||
setUpdatedAnonymizationData,
|
||||
anonymizationFieldsBulkActions,
|
||||
setAnonymizationFieldsBulkActions,
|
||||
setUpdatedKnowledgeBaseSettings,
|
||||
setUpdatedAssistantStreamingEnabled,
|
||||
setUpdatedQuickPromptSettings,
|
||||
|
|
|
@ -12,8 +12,6 @@ import { useAssistantContext } from '../../assistant_context';
|
|||
import { fetchConnectorExecuteAction, FetchConnectorExecuteResponse } from '../api';
|
||||
|
||||
interface SendMessageProps {
|
||||
allow?: string[];
|
||||
allowReplacement?: string[];
|
||||
apiConfig: ApiConfig;
|
||||
http: HttpSetup;
|
||||
message?: string;
|
||||
|
@ -32,14 +30,8 @@ interface UseSendMessage {
|
|||
}
|
||||
|
||||
export const useSendMessage = (): UseSendMessage => {
|
||||
const {
|
||||
alertsIndexPattern,
|
||||
assistantStreamingEnabled,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
knowledgeBase,
|
||||
traceOptions,
|
||||
} = useAssistantContext();
|
||||
const { alertsIndexPattern, assistantStreamingEnabled, knowledgeBase, traceOptions } =
|
||||
useAssistantContext();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const abortController = useRef(new AbortController());
|
||||
const sendMessage = useCallback(
|
||||
|
@ -51,8 +43,6 @@ export const useSendMessage = (): UseSendMessage => {
|
|||
conversationId,
|
||||
isEnabledRAGAlerts: knowledgeBase.isEnabledRAGAlerts, // settings toggle
|
||||
alertsIndexPattern,
|
||||
allow: defaultAllow,
|
||||
allowReplacement: defaultAllowReplacement,
|
||||
apiConfig,
|
||||
isEnabledKnowledgeBase: knowledgeBase.isEnabledKnowledgeBase,
|
||||
assistantStreamingEnabled,
|
||||
|
@ -68,13 +58,11 @@ export const useSendMessage = (): UseSendMessage => {
|
|||
}
|
||||
},
|
||||
[
|
||||
alertsIndexPattern,
|
||||
assistantStreamingEnabled,
|
||||
knowledgeBase.isEnabledRAGAlerts,
|
||||
knowledgeBase.isEnabledKnowledgeBase,
|
||||
knowledgeBase.latestAlerts,
|
||||
alertsIndexPattern,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
assistantStreamingEnabled,
|
||||
traceOptions,
|
||||
]
|
||||
);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { EuiCommentProps } from '@elastic/eui';
|
||||
import type { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { omit, uniq } from 'lodash/fp';
|
||||
import { omit } from 'lodash/fp';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { IToasts } from '@kbn/core-notifications-browser';
|
||||
import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
@ -62,10 +62,6 @@ export interface AssistantProviderProps {
|
|||
currentConversation: Conversation,
|
||||
showAnonymizedValues: boolean
|
||||
) => CodeBlockDetails[][];
|
||||
baseAllow: string[];
|
||||
baseAllowReplacement: string[];
|
||||
defaultAllow: string[];
|
||||
defaultAllowReplacement: string[];
|
||||
basePath: string;
|
||||
basePromptContexts?: PromptContextTemplate[];
|
||||
baseQuickPrompts?: QuickPrompt[];
|
||||
|
@ -85,8 +81,6 @@ export interface AssistantProviderProps {
|
|||
http: HttpSetup;
|
||||
baseConversations: Record<string, Conversation>;
|
||||
nameSpace?: string;
|
||||
setDefaultAllow: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setDefaultAllowReplacement: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
title?: string;
|
||||
toasts?: IToasts;
|
||||
}
|
||||
|
@ -103,11 +97,7 @@ export interface UseAssistantContext {
|
|||
) => CodeBlockDetails[][];
|
||||
allQuickPrompts: QuickPrompt[];
|
||||
allSystemPrompts: Prompt[];
|
||||
baseAllow: string[];
|
||||
baseAllowReplacement: string[];
|
||||
docLinks: Omit<DocLinksStart, 'links'>;
|
||||
defaultAllow: string[];
|
||||
defaultAllowReplacement: string[];
|
||||
basePath: string;
|
||||
basePromptContexts: PromptContextTemplate[];
|
||||
baseQuickPrompts: QuickPrompt[];
|
||||
|
@ -133,8 +123,6 @@ export interface UseAssistantContext {
|
|||
selectedSettingsTab: SettingsTabs;
|
||||
setAllQuickPrompts: React.Dispatch<React.SetStateAction<QuickPrompt[] | undefined>>;
|
||||
setAllSystemPrompts: React.Dispatch<React.SetStateAction<Prompt[] | undefined>>;
|
||||
setDefaultAllow: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setDefaultAllowReplacement: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setAssistantStreamingEnabled: React.Dispatch<React.SetStateAction<boolean | undefined>>;
|
||||
setKnowledgeBase: React.Dispatch<React.SetStateAction<KnowledgeBaseConfig | undefined>>;
|
||||
setLastConversationTitle: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
|
@ -160,10 +148,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
assistantAvailability,
|
||||
assistantTelemetry,
|
||||
augmentMessageCodeBlocks,
|
||||
baseAllow,
|
||||
baseAllowReplacement,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
docLinks,
|
||||
basePath,
|
||||
basePromptContexts = [],
|
||||
|
@ -174,8 +158,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
http,
|
||||
baseConversations,
|
||||
nameSpace = DEFAULT_ASSISTANT_NAMESPACE,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
title = DEFAULT_ASSISTANT_TITLE,
|
||||
toasts,
|
||||
}) => {
|
||||
|
@ -297,14 +279,10 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
augmentMessageCodeBlocks,
|
||||
allQuickPrompts: localStorageQuickPrompts ?? [],
|
||||
allSystemPrompts: localStorageSystemPrompts ?? [],
|
||||
baseAllow: uniq(baseAllow),
|
||||
baseAllowReplacement: uniq(baseAllowReplacement),
|
||||
basePath,
|
||||
basePromptContexts,
|
||||
baseQuickPrompts,
|
||||
baseSystemPrompts,
|
||||
defaultAllow: uniq(defaultAllow),
|
||||
defaultAllowReplacement: uniq(defaultAllowReplacement),
|
||||
docLinks,
|
||||
getComments,
|
||||
http,
|
||||
|
@ -319,8 +297,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
setAssistantStreamingEnabled: setLocalStorageStreaming,
|
||||
setAllQuickPrompts: setLocalStorageQuickPrompts,
|
||||
setAllSystemPrompts: setLocalStorageSystemPrompts,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
setKnowledgeBase: setLocalStorageKnowledgeBase,
|
||||
setSelectedSettingsTab,
|
||||
setShowAssistantOverlay,
|
||||
|
@ -342,14 +318,10 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
augmentMessageCodeBlocks,
|
||||
localStorageQuickPrompts,
|
||||
localStorageSystemPrompts,
|
||||
baseAllow,
|
||||
baseAllowReplacement,
|
||||
basePath,
|
||||
basePromptContexts,
|
||||
baseQuickPrompts,
|
||||
baseSystemPrompts,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
docLinks,
|
||||
getComments,
|
||||
http,
|
||||
|
@ -363,8 +335,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
setLocalStorageStreaming,
|
||||
setLocalStorageQuickPrompts,
|
||||
setLocalStorageSystemPrompts,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
setLocalStorageKnowledgeBase,
|
||||
setSessionStorageTraceOptions,
|
||||
showAssistantOverlay,
|
||||
|
|
|
@ -10,8 +10,17 @@ import { mockAlertPromptContext } from '../../mock/prompt_context';
|
|||
import { getNewSelectedPromptContext } from '.';
|
||||
|
||||
describe('getNewSelectedPromptContext', () => {
|
||||
const defaultAllow = ['field1', 'field2'];
|
||||
const defaultAllowReplacement = ['field3', 'field4'];
|
||||
const anonymizationFields = {
|
||||
total: 4,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{ field: 'field1', id: 'field1', allowed: true, anonymized: false },
|
||||
{ field: 'field2', id: 'field2', allowed: true, anonymized: false },
|
||||
{ field: 'field3', id: 'field3', allowed: false, anonymized: true },
|
||||
{ field: 'field4', id: 'field4', allowed: false, anonymized: true },
|
||||
],
|
||||
};
|
||||
|
||||
it("returns empty `allow` and `allowReplacement` for string `rawData`, because it's not anonymized", async () => {
|
||||
const promptContext: PromptContext = {
|
||||
|
@ -20,14 +29,12 @@ describe('getNewSelectedPromptContext', () => {
|
|||
};
|
||||
|
||||
const result = await getNewSelectedPromptContext({
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
anonymizationFields,
|
||||
promptContext,
|
||||
});
|
||||
|
||||
const excepted: SelectedPromptContext = {
|
||||
allow: [],
|
||||
allowReplacement: [],
|
||||
contextAnonymizationFields: undefined,
|
||||
promptContextId: promptContext.id,
|
||||
rawData: 'string data',
|
||||
};
|
||||
|
@ -42,15 +49,21 @@ describe('getNewSelectedPromptContext', () => {
|
|||
};
|
||||
|
||||
const excepted: SelectedPromptContext = {
|
||||
allow: [...defaultAllow],
|
||||
allowReplacement: [...defaultAllowReplacement],
|
||||
contextAnonymizationFields: {
|
||||
total: 2,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{ field: 'field1', id: 'field1', allowed: true, anonymized: false },
|
||||
{ field: 'field2', id: 'field2', allowed: true, anonymized: false },
|
||||
],
|
||||
},
|
||||
promptContextId: promptContext.id,
|
||||
rawData: { field1: ['value1'], field2: ['value2'] },
|
||||
};
|
||||
|
||||
const result = await getNewSelectedPromptContext({
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
anonymizationFields,
|
||||
promptContext,
|
||||
});
|
||||
|
||||
|
@ -64,8 +77,7 @@ describe('getNewSelectedPromptContext', () => {
|
|||
};
|
||||
|
||||
await getNewSelectedPromptContext({
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
anonymizationFields,
|
||||
promptContext,
|
||||
});
|
||||
|
||||
|
|
|
@ -5,30 +5,49 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
|
||||
import { isAllowed, isAnonymized } from '@kbn/elastic-assistant-common';
|
||||
import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import type { PromptContext, SelectedPromptContext } from '../../assistant/prompt_context/types';
|
||||
|
||||
export async function getNewSelectedPromptContext({
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
anonymizationFields,
|
||||
promptContext,
|
||||
}: {
|
||||
defaultAllow: string[];
|
||||
defaultAllowReplacement: string[];
|
||||
anonymizationFields?: FindAnonymizationFieldsResponse;
|
||||
promptContext: PromptContext;
|
||||
}): Promise<SelectedPromptContext> {
|
||||
const rawData = await promptContext.getPromptContext();
|
||||
|
||||
if (typeof rawData === 'string') {
|
||||
return {
|
||||
allow: [],
|
||||
allowReplacement: [],
|
||||
contextAnonymizationFields: undefined,
|
||||
promptContextId: promptContext.id,
|
||||
rawData,
|
||||
};
|
||||
} else {
|
||||
const extendedAnonymizationData = Object.keys(rawData).reduce<AnonymizationFieldResponse[]>(
|
||||
(acc, field) => [
|
||||
...acc,
|
||||
{
|
||||
id: field,
|
||||
field,
|
||||
allowed: isAllowed({ anonymizationFields: anonymizationFields?.data ?? [], field }),
|
||||
anonymized: isAnonymized({
|
||||
anonymizationFields: anonymizationFields?.data ?? [],
|
||||
field,
|
||||
}),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
return {
|
||||
allow: [...defaultAllow],
|
||||
allowReplacement: [...defaultAllowReplacement],
|
||||
contextAnonymizationFields: {
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
total: extendedAnonymizationData.length,
|
||||
data: extendedAnonymizationData,
|
||||
},
|
||||
promptContextId: promptContext.id,
|
||||
rawData,
|
||||
};
|
||||
|
|
|
@ -6,18 +6,56 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { TestProviders } from '../../../mock/test_providers/test_providers';
|
||||
import { AnonymizationSettings } from '.';
|
||||
import type { Props } from '.';
|
||||
|
||||
const props: Props = {
|
||||
defaultAllow: ['foo', 'bar', 'baz', '@baz'],
|
||||
defaultAllowReplacement: ['bar'],
|
||||
pageSize: 5,
|
||||
setUpdatedDefaultAllow: jest.fn(),
|
||||
setUpdatedDefaultAllowReplacement: jest.fn(),
|
||||
defaultPageSize: 5,
|
||||
anonymizationFields: {
|
||||
total: 4,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{
|
||||
field: 'foo',
|
||||
id: 'test',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
createdAt: '',
|
||||
timestamp: '',
|
||||
},
|
||||
{
|
||||
field: 'bar',
|
||||
id: 'test1',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
createdAt: '',
|
||||
timestamp: '',
|
||||
},
|
||||
{
|
||||
field: 'baz',
|
||||
id: 'test2',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
createdAt: '',
|
||||
timestamp: '',
|
||||
},
|
||||
{
|
||||
field: '@baz',
|
||||
id: 'test3',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
createdAt: '',
|
||||
timestamp: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
setAnonymizationFieldsBulkActions: jest.fn(),
|
||||
setUpdatedAnonymizationData: jest.fn(),
|
||||
anonymizationFieldsBulkActions: {},
|
||||
};
|
||||
|
||||
const mockUseAssistantContext = {
|
||||
|
@ -68,30 +106,6 @@ describe('AnonymizationSettings', () => {
|
|||
expect(getByTestId('contextEditor')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT call `setDefaultAllow` when `Reset` is clicked, because only local state is reset until the user clicks save', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<AnonymizationSettings {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('resetFields'));
|
||||
|
||||
expect(mockUseAssistantContext.setDefaultAllow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does NOT call `setDefaultAllowReplacement` when `Reset` is clicked, because only local state is reset until the user clicks save', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<AnonymizationSettings {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('resetFields'));
|
||||
|
||||
expect(mockUseAssistantContext.setDefaultAllowReplacement).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders the expected allowed stat content', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
|
|
|
@ -5,76 +5,68 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import styled from 'styled-components';
|
||||
import { EuiFlexGroup, EuiHorizontalRule, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { useAssistantContext } from '../../../assistant_context';
|
||||
import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
|
||||
import { PerformBulkActionRequestBody } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { Stats } from '../../../data_anonymization_editor/stats';
|
||||
import { ContextEditor } from '../../../data_anonymization_editor/context_editor';
|
||||
import type { BatchUpdateListItem } from '../../../data_anonymization_editor/context_editor/types';
|
||||
import { updateDefaults } from '../../../data_anonymization_editor/helpers';
|
||||
import { AllowedStat } from '../../../data_anonymization_editor/stats/allowed_stat';
|
||||
import { AnonymizedStat } from '../../../data_anonymization_editor/stats/anonymized_stat';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const StatFlexItem = styled(EuiFlexItem)`
|
||||
margin-right: ${({ theme }) => theme.eui.euiSizeL};
|
||||
`;
|
||||
|
||||
export interface Props {
|
||||
defaultAllow: string[];
|
||||
defaultAllowReplacement: string[];
|
||||
pageSize?: number;
|
||||
setUpdatedDefaultAllow: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setUpdatedDefaultAllowReplacement: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
defaultPageSize?: number;
|
||||
anonymizationFields: FindAnonymizationFieldsResponse;
|
||||
anonymizationFieldsBulkActions: PerformBulkActionRequestBody;
|
||||
setAnonymizationFieldsBulkActions: React.Dispatch<
|
||||
React.SetStateAction<PerformBulkActionRequestBody>
|
||||
>;
|
||||
setUpdatedAnonymizationData: React.Dispatch<
|
||||
React.SetStateAction<FindAnonymizationFieldsResponse>
|
||||
>;
|
||||
}
|
||||
|
||||
const AnonymizationSettingsComponent: React.FC<Props> = ({
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
pageSize,
|
||||
setUpdatedDefaultAllow,
|
||||
setUpdatedDefaultAllowReplacement,
|
||||
defaultPageSize,
|
||||
anonymizationFields,
|
||||
anonymizationFieldsBulkActions,
|
||||
setAnonymizationFieldsBulkActions,
|
||||
setUpdatedAnonymizationData,
|
||||
}) => {
|
||||
const { baseAllow, baseAllowReplacement } = useAssistantContext();
|
||||
|
||||
const onListUpdated = useCallback(
|
||||
(updates: BatchUpdateListItem[]) => {
|
||||
updateDefaults({
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
setDefaultAllow: setUpdatedDefaultAllow,
|
||||
setDefaultAllowReplacement: setUpdatedDefaultAllowReplacement,
|
||||
updates,
|
||||
async (updates: BatchUpdateListItem[]) => {
|
||||
const updatedFieldsKeys = updates.map((u) => u.field);
|
||||
|
||||
const updatedFields = updates.map((u) => ({
|
||||
...(anonymizationFields.data.find((f) => f.field === u.field) ?? { id: '', field: '' }),
|
||||
...(u.update === 'allow' || u.update === 'defaultAllow'
|
||||
? { allowed: u.operation === 'add' }
|
||||
: {}),
|
||||
...(u.update === 'allowReplacement' || u.update === 'defaultAllowReplacement'
|
||||
? { anonymized: u.operation === 'add' }
|
||||
: {}),
|
||||
}));
|
||||
setAnonymizationFieldsBulkActions({
|
||||
...anonymizationFieldsBulkActions,
|
||||
// Only update makes sense now, as long as we don't have an add new field design/UX
|
||||
update: [...(anonymizationFieldsBulkActions?.update ?? []), ...updatedFields],
|
||||
});
|
||||
setUpdatedAnonymizationData({
|
||||
...anonymizationFields,
|
||||
data: [
|
||||
...anonymizationFields.data.filter((f) => !updatedFieldsKeys.includes(f.field)),
|
||||
...updatedFields,
|
||||
],
|
||||
});
|
||||
},
|
||||
[
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
setUpdatedDefaultAllow,
|
||||
setUpdatedDefaultAllowReplacement,
|
||||
anonymizationFields,
|
||||
anonymizationFieldsBulkActions,
|
||||
setAnonymizationFieldsBulkActions,
|
||||
setUpdatedAnonymizationData,
|
||||
]
|
||||
);
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
setUpdatedDefaultAllow(baseAllow);
|
||||
setUpdatedDefaultAllowReplacement(baseAllowReplacement);
|
||||
}, [baseAllow, baseAllowReplacement, setUpdatedDefaultAllow, setUpdatedDefaultAllowReplacement]);
|
||||
|
||||
const anonymized: number = useMemo(() => {
|
||||
const allowSet = new Set(defaultAllow);
|
||||
|
||||
return defaultAllowReplacement.reduce((acc, field) => (allowSet.has(field) ? acc + 1 : acc), 0);
|
||||
}, [defaultAllow, defaultAllowReplacement]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size={'s'}>
|
||||
|
@ -86,24 +78,16 @@ const AnonymizationSettingsComponent: React.FC<Props> = ({
|
|||
<EuiHorizontalRule margin={'s'} />
|
||||
|
||||
<EuiFlexGroup alignItems="center" data-test-subj="summary" gutterSize="none">
|
||||
<StatFlexItem grow={false}>
|
||||
<AllowedStat allowed={defaultAllow.length} total={defaultAllow.length} />
|
||||
</StatFlexItem>
|
||||
|
||||
<StatFlexItem grow={false}>
|
||||
<AnonymizedStat anonymized={anonymized} isDataAnonymizable={true} />
|
||||
</StatFlexItem>
|
||||
<Stats isDataAnonymizable={true} anonymizationFields={anonymizationFields.data} />
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<ContextEditor
|
||||
allow={defaultAllow}
|
||||
allowReplacement={defaultAllowReplacement}
|
||||
anonymizationFields={anonymizationFields}
|
||||
onListUpdated={onListUpdated}
|
||||
onReset={onReset}
|
||||
rawData={null}
|
||||
pageSize={pageSize}
|
||||
pageSize={defaultPageSize}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -86,54 +86,4 @@ describe('BulkActions', () => {
|
|||
{ field: 'user.name', operation: 'remove', update: 'allowReplacement' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('calls onListUpdated with the expected updates when Deny by default is clicked', () => {
|
||||
const { getByTestId, getByText } = render(
|
||||
<BulkActions {...defaultProps} onlyDefaults={true} />
|
||||
);
|
||||
|
||||
userEvent.click(getByTestId('bulkActionsButton'));
|
||||
fireEvent.click(getByText(/^Deny by default$/));
|
||||
|
||||
expect(defaultProps.onListUpdated).toBeCalledWith([
|
||||
{ field: 'process.args', operation: 'remove', update: 'allow' },
|
||||
{ field: 'user.name', operation: 'remove', update: 'allow' },
|
||||
{ field: 'process.args', operation: 'remove', update: 'defaultAllow' },
|
||||
{ field: 'user.name', operation: 'remove', update: 'defaultAllow' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('calls onListUpdated with the expected updates when Anonymize by default is clicked', () => {
|
||||
const { getByTestId, getByText } = render(
|
||||
<BulkActions {...defaultProps} onlyDefaults={true} />
|
||||
);
|
||||
|
||||
userEvent.click(getByTestId('bulkActionsButton'));
|
||||
fireEvent.click(getByText(/^Defaults$/));
|
||||
fireEvent.click(getByText(/^Anonymize by default$/));
|
||||
|
||||
expect(defaultProps.onListUpdated).toBeCalledWith([
|
||||
{ field: 'process.args', operation: 'add', update: 'allowReplacement' },
|
||||
{ field: 'user.name', operation: 'add', update: 'allowReplacement' },
|
||||
{ field: 'process.args', operation: 'add', update: 'defaultAllowReplacement' },
|
||||
{ field: 'user.name', operation: 'add', update: 'defaultAllowReplacement' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('calls onListUpdated with the expected updates when Unanonymize by default is clicked', () => {
|
||||
const { getByTestId, getByText } = render(
|
||||
<BulkActions {...defaultProps} onlyDefaults={true} />
|
||||
);
|
||||
|
||||
userEvent.click(getByTestId('bulkActionsButton'));
|
||||
fireEvent.click(getByText(/^Defaults$/));
|
||||
fireEvent.click(getByText(/^Unanonymize by default$/));
|
||||
|
||||
expect(defaultProps.onListUpdated).toBeCalledWith([
|
||||
{ field: 'process.args', operation: 'remove', update: 'allowReplacement' },
|
||||
{ field: 'user.name', operation: 'remove', update: 'allowReplacement' },
|
||||
{ field: 'process.args', operation: 'remove', update: 'defaultAllowReplacement' },
|
||||
{ field: 'user.name', operation: 'remove', update: 'defaultAllowReplacement' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,7 +27,6 @@ export interface Props {
|
|||
disableDeny?: boolean;
|
||||
disableUnanonymize?: boolean;
|
||||
onListUpdated: (updates: BatchUpdateListItem[]) => void;
|
||||
onlyDefaults: boolean;
|
||||
selected: ContextEditorRow[];
|
||||
}
|
||||
|
||||
|
@ -39,7 +38,6 @@ const BulkActionsComponent: React.FC<Props> = ({
|
|||
disableDeny = false,
|
||||
disableUnanonymize = false,
|
||||
onListUpdated,
|
||||
onlyDefaults,
|
||||
selected,
|
||||
}) => {
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
|
@ -79,7 +77,6 @@ const BulkActionsComponent: React.FC<Props> = ({
|
|||
disableUnanonymize,
|
||||
closePopover,
|
||||
onListUpdated,
|
||||
onlyDefaults,
|
||||
selected,
|
||||
}),
|
||||
[
|
||||
|
@ -89,7 +86,6 @@ const BulkActionsComponent: React.FC<Props> = ({
|
|||
disableDeny,
|
||||
disableUnanonymize,
|
||||
onListUpdated,
|
||||
onlyDefaults,
|
||||
selected,
|
||||
]
|
||||
);
|
||||
|
|
|
@ -133,7 +133,7 @@ describe('getColumns', () => {
|
|||
fireEvent.click(getByTestId('allowed'));
|
||||
|
||||
expect(onListUpdated).toBeCalledWith([
|
||||
{ field: 'event.category', operation: 'remove', update: 'defaultAllowReplacement' },
|
||||
{ field: 'event.category', operation: 'remove', update: 'allow' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -38,7 +38,6 @@ export const getColumns = ({
|
|||
disableAnonymize={!row.allowed || (row.allowed && row.anonymized)}
|
||||
disableUnanonymize={!row.allowed || (row.allowed && !row.anonymized)}
|
||||
onListUpdated={onListUpdated}
|
||||
onlyDefaults={rawData == null}
|
||||
selected={[row]}
|
||||
/>
|
||||
);
|
||||
|
@ -74,17 +73,6 @@ export const getColumns = ({
|
|||
update: rawData == null ? 'defaultAllow' : 'allow',
|
||||
},
|
||||
]);
|
||||
|
||||
if (rawData == null && allowed) {
|
||||
// when editing defaults, remove the default replacement if the field is no longer allowed
|
||||
onListUpdated([
|
||||
{
|
||||
field,
|
||||
operation: 'remove',
|
||||
update: 'defaultAllowReplacement',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { getContextMenuPanels, PRIMARY_PANEL_ID, SECONDARY_PANEL_ID } from '.';
|
||||
import { getContextMenuPanels, PRIMARY_PANEL_ID } from '.';
|
||||
import * as i18n from '../translations';
|
||||
import { ContextEditorRow } from '../types';
|
||||
|
||||
|
@ -23,9 +23,7 @@ describe('getContextMenuPanels', () => {
|
|||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('the first panel has a `primary-panel-id` when onlyDefaults is true', () => {
|
||||
const onlyDefaults = true;
|
||||
|
||||
it('the first panel has a `primary-panel-id`', () => {
|
||||
const panels = getContextMenuPanels({
|
||||
disableAllow: false,
|
||||
disableAnonymize: false,
|
||||
|
@ -34,287 +32,11 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults,
|
||||
});
|
||||
|
||||
expect(panels[0].id).toEqual(PRIMARY_PANEL_ID);
|
||||
});
|
||||
|
||||
it('the first panel also has a `primary-panel-id` when onlyDefaults is false', () => {
|
||||
const onlyDefaults = false;
|
||||
|
||||
const panels = getContextMenuPanels({
|
||||
disableAllow: false,
|
||||
disableAnonymize: false,
|
||||
disableDeny: false,
|
||||
disableUnanonymize: false,
|
||||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults,
|
||||
});
|
||||
|
||||
expect(panels[0].id).toEqual(PRIMARY_PANEL_ID); // first panel is always the primary panel
|
||||
});
|
||||
|
||||
it('the second panel has a `secondary-panel-id` when onlyDefaults is false', () => {
|
||||
const onlyDefaults = false;
|
||||
|
||||
const panels = getContextMenuPanels({
|
||||
disableAllow: false,
|
||||
disableAnonymize: false,
|
||||
disableDeny: false,
|
||||
disableUnanonymize: false,
|
||||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults,
|
||||
});
|
||||
|
||||
expect(panels[1].id).toEqual(SECONDARY_PANEL_ID);
|
||||
});
|
||||
|
||||
it('the second panel is not rendered when onlyDefaults is true', () => {
|
||||
const onlyDefaults = true;
|
||||
|
||||
const panels = getContextMenuPanels({
|
||||
disableAllow: false,
|
||||
disableAnonymize: false,
|
||||
disableDeny: false,
|
||||
disableUnanonymize: false,
|
||||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults,
|
||||
});
|
||||
|
||||
expect(panels.length).toEqual(1);
|
||||
});
|
||||
|
||||
describe('allow by default', () => {
|
||||
it('calls closePopover when allow by default is clicked', () => {
|
||||
const panels = getContextMenuPanels({
|
||||
disableAllow: false,
|
||||
disableAnonymize: false,
|
||||
disableDeny: false,
|
||||
disableUnanonymize: false,
|
||||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const allowByDefaultItem = panels[1].items?.find(
|
||||
(item) => item.name === i18n.ALLOW_BY_DEFAULT
|
||||
);
|
||||
|
||||
allowByDefaultItem?.onClick!(
|
||||
new MouseEvent('click', { bubbles: true }) as unknown as React.MouseEvent<
|
||||
HTMLHRElement,
|
||||
MouseEvent
|
||||
>
|
||||
);
|
||||
|
||||
expect(closePopover).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onListUpdated to add the field to both the `allow` and `defaultAllow` lists', () => {
|
||||
const panels = getContextMenuPanels({
|
||||
disableAllow: false,
|
||||
disableAnonymize: false,
|
||||
disableDeny: false,
|
||||
disableUnanonymize: false,
|
||||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const allowByDefaultItem = panels[1].items?.find(
|
||||
(item) => item.name === i18n.ALLOW_BY_DEFAULT
|
||||
);
|
||||
|
||||
allowByDefaultItem?.onClick!(
|
||||
new MouseEvent('click', { bubbles: true }) as unknown as React.MouseEvent<
|
||||
HTMLHRElement,
|
||||
MouseEvent
|
||||
>
|
||||
);
|
||||
|
||||
expect(onListUpdated).toHaveBeenCalledWith([
|
||||
{ field: 'user.name', operation: 'add', update: 'allow' },
|
||||
{ field: 'user.name', operation: 'add', update: 'defaultAllow' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deny by default', () => {
|
||||
it('calls closePopover when deny by default is clicked', () => {
|
||||
const panels = getContextMenuPanels({
|
||||
disableAllow: false,
|
||||
disableAnonymize: false,
|
||||
disableDeny: false,
|
||||
disableUnanonymize: false,
|
||||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const denyByDefaultItem = panels[1].items?.find((item) => item.name === i18n.DENY_BY_DEFAULT);
|
||||
|
||||
denyByDefaultItem?.onClick!(
|
||||
new MouseEvent('click', { bubbles: true }) as unknown as React.MouseEvent<
|
||||
HTMLHRElement,
|
||||
MouseEvent
|
||||
>
|
||||
);
|
||||
|
||||
expect(closePopover).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onListUpdated to remove the field from both the `allow` and `defaultAllow` lists', () => {
|
||||
const panels = getContextMenuPanels({
|
||||
disableAllow: false,
|
||||
disableAnonymize: false,
|
||||
disableDeny: false,
|
||||
disableUnanonymize: false,
|
||||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const denyByDefaultItem = panels[1].items?.find((item) => item.name === i18n.DENY_BY_DEFAULT);
|
||||
|
||||
denyByDefaultItem?.onClick!(
|
||||
new MouseEvent('click', { bubbles: true }) as unknown as React.MouseEvent<
|
||||
HTMLHRElement,
|
||||
MouseEvent
|
||||
>
|
||||
);
|
||||
|
||||
expect(onListUpdated).toHaveBeenCalledWith([
|
||||
{ field: 'user.name', operation: 'remove', update: 'allow' },
|
||||
{ field: 'user.name', operation: 'remove', update: 'defaultAllow' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('anonymize by default', () => {
|
||||
it('calls closePopover when anonymize by default is clicked', () => {
|
||||
const panels = getContextMenuPanels({
|
||||
disableAllow: false,
|
||||
disableAnonymize: false,
|
||||
disableDeny: false,
|
||||
disableUnanonymize: false,
|
||||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const anonymizeByDefaultItem = panels[1].items?.find(
|
||||
(item) => item.name === i18n.ANONYMIZE_BY_DEFAULT
|
||||
);
|
||||
|
||||
anonymizeByDefaultItem?.onClick!(
|
||||
new MouseEvent('click', { bubbles: true }) as unknown as React.MouseEvent<
|
||||
HTMLHRElement,
|
||||
MouseEvent
|
||||
>
|
||||
);
|
||||
|
||||
expect(closePopover).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onListUpdated to add the field to both the `allowReplacement` and `defaultAllowReplacement` lists', () => {
|
||||
const panels = getContextMenuPanels({
|
||||
disableAllow: false,
|
||||
disableAnonymize: false,
|
||||
disableDeny: false,
|
||||
disableUnanonymize: false,
|
||||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const anonymizeByDefaultItem = panels[1].items?.find(
|
||||
(item) => item.name === i18n.ANONYMIZE_BY_DEFAULT
|
||||
);
|
||||
|
||||
anonymizeByDefaultItem?.onClick!(
|
||||
new MouseEvent('click', { bubbles: true }) as unknown as React.MouseEvent<
|
||||
HTMLHRElement,
|
||||
MouseEvent
|
||||
>
|
||||
);
|
||||
|
||||
expect(onListUpdated).toHaveBeenCalledWith([
|
||||
{ field: 'user.name', operation: 'add', update: 'allowReplacement' },
|
||||
{ field: 'user.name', operation: 'add', update: 'defaultAllowReplacement' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unanonymize by default', () => {
|
||||
it('calls closePopover when unanonymize by default is clicked', () => {
|
||||
const panels = getContextMenuPanels({
|
||||
disableAllow: false,
|
||||
disableAnonymize: false,
|
||||
disableDeny: false,
|
||||
disableUnanonymize: false,
|
||||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const unAnonymizeByDefaultItem = panels[1].items?.find(
|
||||
(item) => item.name === i18n.UNANONYMIZE_BY_DEFAULT
|
||||
);
|
||||
|
||||
unAnonymizeByDefaultItem?.onClick!(
|
||||
new MouseEvent('click', { bubbles: true }) as unknown as React.MouseEvent<
|
||||
HTMLHRElement,
|
||||
MouseEvent
|
||||
>
|
||||
);
|
||||
|
||||
expect(closePopover).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onListUpdated to remove the field from both the `allowReplacement` and `defaultAllowReplacement` lists', () => {
|
||||
const panels = getContextMenuPanels({
|
||||
disableAllow: false,
|
||||
disableAnonymize: false,
|
||||
disableDeny: false,
|
||||
disableUnanonymize: false,
|
||||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const unAnonymizeByDefaultItem = panels[1].items?.find(
|
||||
(item) => item.name === i18n.UNANONYMIZE_BY_DEFAULT
|
||||
);
|
||||
|
||||
unAnonymizeByDefaultItem?.onClick!(
|
||||
new MouseEvent('click', { bubbles: true }) as unknown as React.MouseEvent<
|
||||
HTMLHRElement,
|
||||
MouseEvent
|
||||
>
|
||||
);
|
||||
|
||||
expect(onListUpdated).toHaveBeenCalledWith([
|
||||
{ field: 'user.name', operation: 'remove', update: 'allowReplacement' },
|
||||
{ field: 'user.name', operation: 'remove', update: 'defaultAllowReplacement' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('allow', () => {
|
||||
it('is disabled when `disableAlow` is true', () => {
|
||||
const disableAllow = true;
|
||||
|
@ -327,7 +49,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const allowItem = panels[0].items?.find((item) => item.name === i18n.ALLOW);
|
||||
|
@ -346,7 +67,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const allowItem = panels[0].items?.find((item) => item.name === i18n.ALLOW);
|
||||
|
@ -363,7 +83,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const allowItem = panels[0].items?.find((item) => item.name === i18n.ALLOW);
|
||||
|
@ -387,7 +106,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const allowItem = panels[0].items?.find((item) => item.name === i18n.ALLOW);
|
||||
|
@ -417,7 +135,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const denyItem = panels[0].items?.find((item) => item.name === i18n.DENY);
|
||||
|
@ -436,7 +153,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const denyItem = panels[0].items?.find((item) => item.name === i18n.DENY);
|
||||
|
@ -453,7 +169,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const denyByDefaultItem = panels[0].items?.find((item) => item.name === i18n.DENY);
|
||||
|
@ -477,7 +192,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const denyItem = panels[0].items?.find((item) => item.name === i18n.DENY);
|
||||
|
@ -507,7 +221,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const anonymizeItem = panels[0].items?.find((item) => item.name === i18n.ANONYMIZE);
|
||||
|
@ -526,7 +239,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const anonymizeItem = panels[0].items?.find((item) => item.name === i18n.ANONYMIZE);
|
||||
|
@ -543,7 +255,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const anonymizeItem = panels[0].items?.find((item) => item.name === i18n.ANONYMIZE);
|
||||
|
@ -567,7 +278,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const anonymizeItem = panels[0].items?.find((item) => item.name === i18n.ANONYMIZE);
|
||||
|
@ -597,7 +307,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const unanonymizeItem = panels[0].items?.find((item) => item.name === i18n.UNANONYMIZE);
|
||||
|
@ -616,7 +325,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const unanonymizeItem = panels[0].items?.find((item) => item.name === i18n.UNANONYMIZE);
|
||||
|
@ -633,7 +341,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const unAnonymizeItem = panels[0].items?.find((item) => item.name === i18n.UNANONYMIZE);
|
||||
|
@ -657,7 +364,6 @@ describe('getContextMenuPanels', () => {
|
|||
closePopover,
|
||||
onListUpdated,
|
||||
selected,
|
||||
onlyDefaults: false,
|
||||
});
|
||||
|
||||
const unAnonymizeItem = panels[0].items?.find((item) => item.name === i18n.UNANONYMIZE);
|
||||
|
|
|
@ -11,7 +11,6 @@ import * as i18n from '../translations';
|
|||
import { BatchUpdateListItem, ContextEditorRow } from '../types';
|
||||
|
||||
export const PRIMARY_PANEL_ID = 'primary-panel-id';
|
||||
export const SECONDARY_PANEL_ID = 'secondary-panel-id';
|
||||
|
||||
export const getContextMenuPanels = ({
|
||||
disableAllow,
|
||||
|
@ -20,7 +19,6 @@ export const getContextMenuPanels = ({
|
|||
disableUnanonymize,
|
||||
closePopover,
|
||||
onListUpdated,
|
||||
onlyDefaults,
|
||||
selected,
|
||||
}: {
|
||||
disableAllow: boolean;
|
||||
|
@ -30,113 +28,8 @@ export const getContextMenuPanels = ({
|
|||
closePopover: () => void;
|
||||
onListUpdated: (updates: BatchUpdateListItem[]) => void;
|
||||
selected: ContextEditorRow[];
|
||||
onlyDefaults: boolean;
|
||||
}): EuiContextMenuPanelDescriptor[] => {
|
||||
const defaultsPanelId = onlyDefaults ? PRIMARY_PANEL_ID : SECONDARY_PANEL_ID;
|
||||
const nonDefaultsPanelId = onlyDefaults ? SECONDARY_PANEL_ID : PRIMARY_PANEL_ID;
|
||||
|
||||
const allowByDefault = [
|
||||
!onlyDefaults
|
||||
? {
|
||||
icon: 'check',
|
||||
name: i18n.ALLOW_BY_DEFAULT,
|
||||
onClick: () => {
|
||||
closePopover();
|
||||
|
||||
const updateAllow = selected.map<BatchUpdateListItem>(({ field }) => ({
|
||||
field,
|
||||
operation: 'add',
|
||||
update: 'allow',
|
||||
}));
|
||||
|
||||
const updateDefaultAllow = selected.map<BatchUpdateListItem>(({ field }) => ({
|
||||
field,
|
||||
operation: 'add',
|
||||
update: 'defaultAllow',
|
||||
}));
|
||||
|
||||
onListUpdated([...updateAllow, ...updateDefaultAllow]);
|
||||
},
|
||||
}
|
||||
: [],
|
||||
].flat();
|
||||
|
||||
const defaultsPanelItems: EuiContextMenuPanelDescriptor[] = [
|
||||
{
|
||||
id: defaultsPanelId,
|
||||
title: i18n.DEFAULTS,
|
||||
items: [
|
||||
...allowByDefault,
|
||||
{
|
||||
icon: 'cross',
|
||||
name: i18n.DENY_BY_DEFAULT,
|
||||
onClick: () => {
|
||||
closePopover();
|
||||
|
||||
const updateAllow = selected.map<BatchUpdateListItem>(({ field }) => ({
|
||||
field,
|
||||
operation: 'remove',
|
||||
update: 'allow',
|
||||
}));
|
||||
|
||||
const updateDefaultAllow = selected.map<BatchUpdateListItem>(({ field }) => ({
|
||||
field,
|
||||
operation: 'remove',
|
||||
update: 'defaultAllow',
|
||||
}));
|
||||
|
||||
onListUpdated([...updateAllow, ...updateDefaultAllow]);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'eyeClosed',
|
||||
name: i18n.ANONYMIZE_BY_DEFAULT,
|
||||
onClick: () => {
|
||||
closePopover();
|
||||
|
||||
const updateAllowReplacement = selected.map<BatchUpdateListItem>(({ field }) => ({
|
||||
field,
|
||||
operation: 'add',
|
||||
update: 'allowReplacement',
|
||||
}));
|
||||
|
||||
const updateDefaultAllowReplacement = selected.map<BatchUpdateListItem>(
|
||||
({ field }) => ({
|
||||
field,
|
||||
operation: 'add',
|
||||
update: 'defaultAllowReplacement',
|
||||
})
|
||||
);
|
||||
|
||||
onListUpdated([...updateAllowReplacement, ...updateDefaultAllowReplacement]);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'eye',
|
||||
name: i18n.UNANONYMIZE_BY_DEFAULT,
|
||||
onClick: () => {
|
||||
closePopover();
|
||||
|
||||
const updateAllowReplacement = selected.map<BatchUpdateListItem>(({ field }) => ({
|
||||
field,
|
||||
operation: 'remove',
|
||||
update: 'allowReplacement',
|
||||
}));
|
||||
|
||||
const updateDefaultAllowReplacement = selected.map<BatchUpdateListItem>(
|
||||
({ field }) => ({
|
||||
field,
|
||||
operation: 'remove',
|
||||
update: 'defaultAllowReplacement',
|
||||
})
|
||||
);
|
||||
|
||||
onListUpdated([...updateAllowReplacement, ...updateDefaultAllowReplacement]);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const nonDefaultsPanelId = PRIMARY_PANEL_ID;
|
||||
|
||||
const nonDefaultsPanelItems: EuiContextMenuPanelDescriptor[] = [
|
||||
{
|
||||
|
@ -210,14 +103,9 @@ export const getContextMenuPanels = ({
|
|||
isSeparator: true,
|
||||
key: 'sep',
|
||||
},
|
||||
{
|
||||
name: i18n.DEFAULTS,
|
||||
panel: defaultsPanelId,
|
||||
},
|
||||
],
|
||||
},
|
||||
...defaultsPanelItems,
|
||||
];
|
||||
|
||||
return onlyDefaults ? defaultsPanelItems : nonDefaultsPanelItems;
|
||||
return nonDefaultsPanelItems;
|
||||
};
|
||||
|
|
|
@ -11,12 +11,48 @@ import { getRows } from '.';
|
|||
|
||||
describe('getRows', () => {
|
||||
const defaultArgs: {
|
||||
allow: SelectedPromptContext['allow'];
|
||||
allowReplacement: SelectedPromptContext['allowReplacement'];
|
||||
anonymizationFields: SelectedPromptContext['contextAnonymizationFields'];
|
||||
rawData: Record<string, string[]> | null;
|
||||
} = {
|
||||
allow: ['event.action', 'user.name', 'other.field'], // other.field is not in the rawData
|
||||
allowReplacement: ['user.name', 'host.ip'], // host.ip is not in the rawData
|
||||
anonymizationFields: {
|
||||
total: 4,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{
|
||||
field: 'event.action',
|
||||
id: 'test',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
createdAt: '',
|
||||
timestamp: '',
|
||||
},
|
||||
{
|
||||
field: 'user.name',
|
||||
id: 'test1',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
createdAt: '',
|
||||
timestamp: '',
|
||||
},
|
||||
{
|
||||
field: 'other.field',
|
||||
id: 'test2',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
createdAt: '',
|
||||
timestamp: '',
|
||||
},
|
||||
{
|
||||
field: 'host.ip',
|
||||
id: 'test3',
|
||||
allowed: false,
|
||||
anonymized: true,
|
||||
createdAt: '',
|
||||
timestamp: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
rawData: {
|
||||
'event.category': ['process'], // event.category is not in the allow list, nor is it in the allowReplacement list
|
||||
'event.action': ['process_stopped', 'stop'], // event.action is in the allow list, but not the allowReplacement list
|
||||
|
@ -25,33 +61,8 @@ describe('getRows', () => {
|
|||
};
|
||||
|
||||
it('returns only the allowed fields if no rawData is provided', () => {
|
||||
const expected: ContextEditorRow[] = [
|
||||
{
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
denied: false,
|
||||
field: 'event.action',
|
||||
rawValues: [],
|
||||
},
|
||||
{
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
denied: false,
|
||||
field: 'other.field',
|
||||
rawValues: [],
|
||||
},
|
||||
{
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
denied: false,
|
||||
field: 'user.name',
|
||||
rawValues: [],
|
||||
},
|
||||
];
|
||||
|
||||
const nullRawData: {
|
||||
allow: SelectedPromptContext['allow'];
|
||||
allowReplacement: SelectedPromptContext['allowReplacement'];
|
||||
anonymizationFields: SelectedPromptContext['contextAnonymizationFields'];
|
||||
rawData: Record<string, string[]> | null;
|
||||
} = {
|
||||
...defaultArgs,
|
||||
|
@ -60,7 +71,9 @@ describe('getRows', () => {
|
|||
|
||||
const rows = getRows(nullRawData);
|
||||
|
||||
expect(rows).toEqual(expected);
|
||||
expect(JSON.stringify(rows)).toEqual(
|
||||
'[{"field":"event.action","allowed":true,"anonymized":false,"denied":false,"rawValues":[]},{"field":"user.name","allowed":true,"anonymized":true,"denied":false,"rawValues":[]},{"field":"other.field","allowed":true,"anonymized":false,"denied":false,"rawValues":[]},{"field":"host.ip","allowed":false,"anonymized":true,"denied":true,"rawValues":[]}]'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the expected metadata and raw values', () => {
|
||||
|
|
|
@ -5,23 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
|
||||
import { isAllowed, isAnonymized, isDenied } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import { SelectedPromptContext } from '../../../assistant/prompt_context/types';
|
||||
import { ContextEditorRow } from '../types';
|
||||
|
||||
export const getRows = ({
|
||||
allow,
|
||||
allowReplacement,
|
||||
anonymizationFields,
|
||||
rawData,
|
||||
}: {
|
||||
allow: SelectedPromptContext['allow'];
|
||||
allowReplacement: SelectedPromptContext['allowReplacement'];
|
||||
anonymizationFields?: FindAnonymizationFieldsResponse;
|
||||
rawData: Record<string, string[]> | null;
|
||||
}): ContextEditorRow[] => {
|
||||
const allowReplacementSet = new Set(allowReplacement);
|
||||
const allowSet = new Set(allow);
|
||||
|
||||
if (rawData !== null && typeof rawData === 'object') {
|
||||
const rawFields = Object.keys(rawData).sort();
|
||||
|
||||
|
@ -30,27 +24,21 @@ export const getRows = ({
|
|||
...acc,
|
||||
{
|
||||
field,
|
||||
allowed: isAllowed({ allowSet, field }),
|
||||
anonymized: isAnonymized({ allowReplacementSet, field }),
|
||||
denied: isDenied({ allowSet, field }),
|
||||
allowed: isAllowed({ anonymizationFields: anonymizationFields?.data ?? [], field }),
|
||||
anonymized: isAnonymized({ anonymizationFields: anonymizationFields?.data ?? [], field }),
|
||||
denied: isDenied({ anonymizationFields: anonymizationFields?.data ?? [], field }),
|
||||
rawValues: rawData[field],
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
} else {
|
||||
return allow.sort().reduce<ContextEditorRow[]>(
|
||||
(acc, field) => [
|
||||
...acc,
|
||||
{
|
||||
field,
|
||||
allowed: true,
|
||||
anonymized: allowReplacementSet.has(field),
|
||||
denied: false,
|
||||
rawValues: [],
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
return (anonymizationFields?.data ?? []).map<ContextEditorRow>((anonymizationField) => ({
|
||||
field: anonymizationField.field,
|
||||
allowed: anonymizationField.allowed ?? false,
|
||||
anonymized: anonymizationField.anonymized ?? false,
|
||||
denied: !(anonymizationField.allowed ?? false),
|
||||
rawValues: [],
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -13,7 +13,12 @@ import { ContextEditor } from '.';
|
|||
|
||||
describe('ContextEditor', () => {
|
||||
const allow = Array.from({ length: 20 }, (_, i) => `field${i + 1}`);
|
||||
const allowReplacement = ['field1'];
|
||||
const anonymizationFields = {
|
||||
total: 20,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: allow.map((f) => ({ id: f, field: f, allowed: true, anonymized: f === 'field1' })),
|
||||
};
|
||||
const rawData = allow.reduce(
|
||||
(acc, field, index) => ({ ...acc, [field]: [`value${index + 1}`] }),
|
||||
{}
|
||||
|
@ -26,8 +31,7 @@ describe('ContextEditor', () => {
|
|||
|
||||
render(
|
||||
<ContextEditor
|
||||
allow={allow}
|
||||
allowReplacement={allowReplacement}
|
||||
anonymizationFields={anonymizationFields}
|
||||
onListUpdated={onListUpdated}
|
||||
rawData={rawData}
|
||||
/>
|
||||
|
|
|
@ -9,6 +9,7 @@ import { EuiInMemoryTable } from '@elastic/eui';
|
|||
import type { EuiSearchBarProps, EuiTableSelectionType } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState, useRef } from 'react';
|
||||
|
||||
import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
|
||||
import { getColumns } from './get_columns';
|
||||
import { getRows } from './get_rows';
|
||||
import { Toolbar } from './toolbar';
|
||||
|
@ -19,16 +20,14 @@ export const DEFAULT_PAGE_SIZE = 10;
|
|||
|
||||
const defaultSort: SortConfig = {
|
||||
sort: {
|
||||
direction: 'desc',
|
||||
field: FIELDS.ALLOWED,
|
||||
direction: 'asc',
|
||||
field: FIELDS.FIELD,
|
||||
},
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
allow: string[];
|
||||
allowReplacement: string[];
|
||||
anonymizationFields: FindAnonymizationFieldsResponse;
|
||||
onListUpdated: (updates: BatchUpdateListItem[]) => void;
|
||||
onReset?: () => void;
|
||||
rawData: Record<string, string[]> | null;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
@ -52,10 +51,8 @@ const search: EuiSearchBarProps = {
|
|||
};
|
||||
|
||||
const ContextEditorComponent: React.FC<Props> = ({
|
||||
allow,
|
||||
allowReplacement,
|
||||
anonymizationFields,
|
||||
onListUpdated,
|
||||
onReset,
|
||||
rawData,
|
||||
pageSize = DEFAULT_PAGE_SIZE,
|
||||
}) => {
|
||||
|
@ -84,11 +81,10 @@ const ContextEditorComponent: React.FC<Props> = ({
|
|||
const rows = useMemo(
|
||||
() =>
|
||||
getRows({
|
||||
allow,
|
||||
allowReplacement,
|
||||
anonymizationFields,
|
||||
rawData,
|
||||
}),
|
||||
[allow, allowReplacement, rawData]
|
||||
[anonymizationFields, rawData]
|
||||
);
|
||||
|
||||
const onSelectAll = useCallback(() => {
|
||||
|
@ -107,14 +103,12 @@ const ContextEditorComponent: React.FC<Props> = ({
|
|||
() => (
|
||||
<Toolbar
|
||||
onListUpdated={onListUpdated}
|
||||
onlyDefaults={rawData == null}
|
||||
onReset={onReset}
|
||||
onSelectAll={onSelectAll}
|
||||
selected={selected}
|
||||
totalFields={rows.length}
|
||||
totalFields={rawData == null ? anonymizationFields.total : Object.keys(rawData).length}
|
||||
/>
|
||||
),
|
||||
[onListUpdated, onReset, onSelectAll, rawData, rows, selected]
|
||||
[anonymizationFields.total, onListUpdated, onSelectAll, rawData, selected]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -40,7 +40,6 @@ describe('Toolbar', () => {
|
|||
const defaultProps = {
|
||||
onListUpdated: jest.fn(),
|
||||
onlyDefaults: false,
|
||||
onReset: jest.fn(),
|
||||
onSelectAll: jest.fn(),
|
||||
selected: [], // no rows selected
|
||||
totalFields: 5,
|
||||
|
|
|
@ -14,8 +14,6 @@ import { BatchUpdateListItem, ContextEditorRow } from '../types';
|
|||
|
||||
export interface Props {
|
||||
onListUpdated: (updates: BatchUpdateListItem[]) => void;
|
||||
onlyDefaults: boolean;
|
||||
onReset?: () => void;
|
||||
onSelectAll: () => void;
|
||||
selected: ContextEditorRow[];
|
||||
totalFields: number;
|
||||
|
@ -23,8 +21,6 @@ export interface Props {
|
|||
|
||||
const ToolbarComponent: React.FC<Props> = ({
|
||||
onListUpdated,
|
||||
onlyDefaults,
|
||||
onReset,
|
||||
onSelectAll,
|
||||
selected,
|
||||
totalFields,
|
||||
|
@ -52,32 +48,9 @@ const ToolbarComponent: React.FC<Props> = ({
|
|||
appliesTo="multipleRows"
|
||||
disabled={selected.length === 0}
|
||||
onListUpdated={onListUpdated}
|
||||
onlyDefaults={onlyDefaults}
|
||||
selected={selected}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
{onReset != null && (
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
data-test-subj="toolbarTrailingActions"
|
||||
gutterSize="none"
|
||||
justifyContent="flexEnd"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="resetFields"
|
||||
iconType="eraser"
|
||||
onClick={onReset}
|
||||
size="xs"
|
||||
>
|
||||
{i18n.RESET}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
|
|
|
@ -5,16 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SelectedPromptContext } from '../../assistant/prompt_context/types';
|
||||
import type { Stats } from '../helpers';
|
||||
import { getStats } from '.';
|
||||
|
||||
describe('getStats', () => {
|
||||
it('returns ZERO_STATS for string rawData', () => {
|
||||
const context: SelectedPromptContext = {
|
||||
allow: [],
|
||||
allowReplacement: [],
|
||||
promptContextId: 'abcd',
|
||||
const context = {
|
||||
anonymizationFields: [],
|
||||
rawData: 'this will not be anonymized',
|
||||
};
|
||||
|
||||
|
@ -29,10 +26,41 @@ describe('getStats', () => {
|
|||
});
|
||||
|
||||
it('returns the expected stats for object rawData', () => {
|
||||
const context: SelectedPromptContext = {
|
||||
allow: ['event.category', 'event.action', 'user.name'],
|
||||
allowReplacement: ['user.name', 'host.ip'], // only user.name is allowed to be sent
|
||||
promptContextId: 'abcd',
|
||||
const context = {
|
||||
anonymizationFields: [
|
||||
{
|
||||
field: 'event.action',
|
||||
id: 'test',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
createdAt: '',
|
||||
timestamp: '',
|
||||
},
|
||||
{
|
||||
field: 'user.name',
|
||||
id: 'test1',
|
||||
allowed: true,
|
||||
anonymized: true,
|
||||
createdAt: '',
|
||||
timestamp: '',
|
||||
},
|
||||
{
|
||||
field: 'event.category',
|
||||
id: 'test2',
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
createdAt: '',
|
||||
timestamp: '',
|
||||
},
|
||||
{
|
||||
field: 'host.ip',
|
||||
id: 'test3',
|
||||
allowed: false,
|
||||
anonymized: true,
|
||||
createdAt: '',
|
||||
timestamp: '',
|
||||
},
|
||||
],
|
||||
rawData: {
|
||||
'event.category': ['process'],
|
||||
'event.action': ['process_stopped'],
|
||||
|
|
|
@ -7,10 +7,16 @@
|
|||
|
||||
import { isAllowed, isAnonymized, isDenied } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import type { SelectedPromptContext } from '../../assistant/prompt_context/types';
|
||||
import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { Stats } from '../helpers';
|
||||
|
||||
export const getStats = ({ allow, allowReplacement, rawData }: SelectedPromptContext): Stats => {
|
||||
export const getStats = ({
|
||||
anonymizationFields = [],
|
||||
rawData,
|
||||
}: {
|
||||
anonymizationFields?: AnonymizationFieldResponse[];
|
||||
rawData?: string | Record<string, string[]>;
|
||||
}): Stats => {
|
||||
const ZERO_STATS = {
|
||||
allowed: 0,
|
||||
anonymized: 0,
|
||||
|
@ -18,21 +24,30 @@ export const getStats = ({ allow, allowReplacement, rawData }: SelectedPromptCon
|
|||
total: 0,
|
||||
};
|
||||
|
||||
if (typeof rawData === 'string') {
|
||||
if (!rawData) {
|
||||
return {
|
||||
allowed: anonymizationFields.reduce((acc, data) => (data.allowed ? acc + 1 : acc), 0),
|
||||
anonymized: anonymizationFields.reduce((acc, data) => (data.anonymized ? acc + 1 : acc), 0),
|
||||
denied: anonymizationFields.reduce(
|
||||
(acc, data) => (data.allowed === false ? acc + 1 : acc),
|
||||
0
|
||||
),
|
||||
total: anonymizationFields.length,
|
||||
};
|
||||
} else if (typeof rawData === 'string') {
|
||||
return ZERO_STATS;
|
||||
} else {
|
||||
const rawFields = Object.keys(rawData);
|
||||
|
||||
const allowReplacementSet = new Set(allowReplacement);
|
||||
const allowSet = new Set(allow);
|
||||
|
||||
return rawFields.reduce<Stats>(
|
||||
(acc, field) => ({
|
||||
allowed: acc.allowed + (isAllowed({ allowSet, field }) ? 1 : 0),
|
||||
allowed: acc.allowed + (isAllowed({ anonymizationFields, field }) ? 1 : 0),
|
||||
anonymized:
|
||||
acc.anonymized +
|
||||
(isAllowed({ allowSet, field }) && isAnonymized({ allowReplacementSet, field }) ? 1 : 0),
|
||||
denied: acc.denied + (isDenied({ allowSet, field }) ? 1 : 0),
|
||||
(isAllowed({ anonymizationFields, field }) && isAnonymized({ anonymizationFields, field })
|
||||
? 1
|
||||
: 0),
|
||||
denied: acc.denied + (isDenied({ anonymizationFields, field }) ? 1 : 0),
|
||||
total: acc.total + 1,
|
||||
}),
|
||||
ZERO_STATS
|
||||
|
|
|
@ -5,18 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isAllowed, isAnonymized, isDenied } from '@kbn/elastic-assistant-common';
|
||||
import { getIsDataAnonymizable, updateSelectedPromptContext } from '.';
|
||||
import { SelectedPromptContext } from '../../assistant/prompt_context/types';
|
||||
import {
|
||||
isAllowed,
|
||||
isAnonymized,
|
||||
isDenied,
|
||||
getIsDataAnonymizable,
|
||||
updateDefaultList,
|
||||
updateDefaults,
|
||||
updateList,
|
||||
updateSelectedPromptContext,
|
||||
} from '.';
|
||||
import { BatchUpdateListItem } from '../context_editor/types';
|
||||
|
||||
describe('helpers', () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
@ -41,117 +32,227 @@ describe('helpers', () => {
|
|||
|
||||
describe('isAllowed', () => {
|
||||
it('returns true when the field is present in the allowSet', () => {
|
||||
const allowSet = new Set(['fieldName1', 'fieldName2', 'fieldName3']);
|
||||
const anonymizationFields = {
|
||||
total: 3,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{
|
||||
id: 'fieldName1',
|
||||
field: 'fieldName1',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
id: 'fieldName2',
|
||||
field: 'fieldName2',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
id: 'fieldName3',
|
||||
field: 'fieldName3',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(isAllowed({ allowSet, field: 'fieldName1' })).toBe(true);
|
||||
expect(
|
||||
isAllowed({ anonymizationFields: anonymizationFields.data, field: 'fieldName1' })
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the field is NOT present in the allowSet', () => {
|
||||
const allowSet = new Set(['fieldName1', 'fieldName2', 'fieldName3']);
|
||||
const anonymizationFields = {
|
||||
total: 3,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{
|
||||
id: 'fieldName1',
|
||||
field: 'fieldName1',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
id: 'fieldName2',
|
||||
field: 'fieldName2',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
id: 'fieldName3',
|
||||
field: 'fieldName3',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(isAllowed({ allowSet, field: 'nonexistentField' })).toBe(false);
|
||||
expect(
|
||||
isAllowed({ anonymizationFields: anonymizationFields.data, field: 'nonexistentField' })
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDenied', () => {
|
||||
it('returns true when the field is NOT in the allowSet', () => {
|
||||
const allowSet = new Set(['field1', 'field2']);
|
||||
const anonymizationFields = {
|
||||
total: 2,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{
|
||||
id: 'field1',
|
||||
field: 'field1',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
id: 'field2',
|
||||
field: 'field2',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const field = 'field3';
|
||||
|
||||
expect(isDenied({ allowSet, field })).toBe(true);
|
||||
expect(isDenied({ anonymizationFields: anonymizationFields.data, field })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the field is in the allowSet', () => {
|
||||
const allowSet = new Set(['field1', 'field2']);
|
||||
const anonymizationFields = {
|
||||
total: 2,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{
|
||||
id: 'field1',
|
||||
field: 'field1',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
id: 'field2',
|
||||
field: 'field2',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
const field = 'field1';
|
||||
|
||||
expect(isDenied({ allowSet, field })).toBe(false);
|
||||
expect(isDenied({ anonymizationFields: anonymizationFields.data, field })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for an empty allowSet', () => {
|
||||
const allowSet = new Set<string>();
|
||||
const anonymizationFields = {
|
||||
total: 0,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [],
|
||||
};
|
||||
const field = 'field1';
|
||||
|
||||
expect(isDenied({ allowSet, field })).toBe(true);
|
||||
expect(isDenied({ anonymizationFields: anonymizationFields.data, field })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the field is an empty string and allowSet contains the empty string', () => {
|
||||
const allowSet = new Set(['', 'field1']);
|
||||
const anonymizationFields = {
|
||||
total: 2,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{
|
||||
id: 'field1',
|
||||
field: 'field1',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
id: '',
|
||||
field: '',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
const field = '';
|
||||
|
||||
expect(isDenied({ allowSet, field })).toBe(false);
|
||||
expect(isDenied({ anonymizationFields: anonymizationFields.data, field })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAnonymized', () => {
|
||||
const allowReplacementSet = new Set(['user.name', 'host.name']);
|
||||
const anonymizationFields = {
|
||||
total: 2,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{
|
||||
id: 'user.name',
|
||||
field: 'user.name',
|
||||
anonymized: true,
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
id: 'host.name',
|
||||
field: 'host.name',
|
||||
anonymized: true,
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('returns true when the field is in the allowReplacementSet', () => {
|
||||
const field = 'user.name';
|
||||
|
||||
expect(isAnonymized({ allowReplacementSet, field })).toBe(true);
|
||||
expect(isAnonymized({ anonymizationFields: anonymizationFields.data, field })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the field is NOT in the allowReplacementSet', () => {
|
||||
const field = 'foozle';
|
||||
|
||||
expect(isAnonymized({ allowReplacementSet, field })).toBe(false);
|
||||
expect(isAnonymized({ anonymizationFields: anonymizationFields.data, field })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when allowReplacementSet is empty', () => {
|
||||
const emptySet = new Set<string>();
|
||||
const field = 'user.name';
|
||||
|
||||
expect(isAnonymized({ allowReplacementSet: emptySet, field })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateList', () => {
|
||||
it('adds a new field to the list when the operation is `add`', () => {
|
||||
const result = updateList({
|
||||
field: 'newField',
|
||||
list: ['field1', 'field2'],
|
||||
operation: 'add',
|
||||
});
|
||||
|
||||
expect(result).toEqual(['field1', 'field2', 'newField']);
|
||||
});
|
||||
|
||||
it('does NOT add a duplicate field to the list when the operation is `add`', () => {
|
||||
const result = updateList({
|
||||
field: 'field1',
|
||||
list: ['field1', 'field2'],
|
||||
operation: 'add',
|
||||
});
|
||||
|
||||
expect(result).toEqual(['field1', 'field2']);
|
||||
});
|
||||
|
||||
it('removes an existing field from the list when the operation is `remove`', () => {
|
||||
const result = updateList({
|
||||
field: 'field1',
|
||||
list: ['field1', 'field2'],
|
||||
operation: 'remove',
|
||||
});
|
||||
|
||||
expect(result).toEqual(['field2']);
|
||||
});
|
||||
|
||||
it('should NOT modify the list when removing a non-existent field', () => {
|
||||
const result = updateList({
|
||||
field: 'host.name',
|
||||
list: ['field1', 'field2'],
|
||||
operation: 'remove',
|
||||
});
|
||||
|
||||
expect(result).toEqual(['field1', 'field2']);
|
||||
expect(isAnonymized({ anonymizationFields: [], field })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSelectedPromptContext', () => {
|
||||
const selectedPromptContext: SelectedPromptContext = {
|
||||
allow: ['user.name', 'event.category'],
|
||||
allowReplacement: ['user.name'],
|
||||
contextAnonymizationFields: {
|
||||
total: 2,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{
|
||||
id: 'user.name',
|
||||
field: 'user.name',
|
||||
anonymized: true,
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
id: 'event.category',
|
||||
field: 'event.category',
|
||||
anonymized: true,
|
||||
allowed: false,
|
||||
},
|
||||
{
|
||||
id: 'event.action',
|
||||
field: 'event.action',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
promptContextId: 'testId',
|
||||
rawData: {},
|
||||
};
|
||||
|
@ -164,7 +265,13 @@ describe('helpers', () => {
|
|||
update: 'allow',
|
||||
});
|
||||
|
||||
expect(result.allow).toEqual(['user.name', 'event.category', 'event.action']);
|
||||
expect(
|
||||
result.contextAnonymizationFields?.data.sort((a, b) => (a.field > b.field ? -1 : 1))
|
||||
).toEqual([
|
||||
{ id: 'user.name', field: 'user.name', anonymized: true, allowed: true },
|
||||
{ id: 'event.category', field: 'event.category', anonymized: true, allowed: false },
|
||||
{ id: 'event.action', field: 'event.action', anonymized: false, allowed: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it('updates the allow list when update is `allow` and the operation is `remove`', () => {
|
||||
|
@ -175,17 +282,29 @@ describe('helpers', () => {
|
|||
update: 'allow',
|
||||
});
|
||||
|
||||
expect(result.allow).toEqual(['event.category']);
|
||||
expect(
|
||||
result.contextAnonymizationFields?.data.sort((a, b) => (a.field > b.field ? -1 : 1))
|
||||
).toEqual([
|
||||
{ allowed: false, anonymized: true, field: 'user.name', id: 'user.name' },
|
||||
{ allowed: false, anonymized: true, field: 'event.category', id: 'event.category' },
|
||||
{ allowed: true, anonymized: false, field: 'event.action', id: 'event.action' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('updates the allowReplacement list when update is `allowReplacement` and the operation is `add`', () => {
|
||||
const result = updateSelectedPromptContext({
|
||||
field: 'event.type',
|
||||
field: 'event.category',
|
||||
operation: 'add',
|
||||
selectedPromptContext,
|
||||
update: 'allowReplacement',
|
||||
});
|
||||
expect(result.allowReplacement).toEqual(['user.name', 'event.type']);
|
||||
expect(
|
||||
result.contextAnonymizationFields?.data.sort((a, b) => (a.field > b.field ? -1 : 1))
|
||||
).toEqual([
|
||||
{ allowed: true, anonymized: true, field: 'user.name', id: 'user.name' },
|
||||
{ allowed: false, anonymized: true, field: 'event.category', id: 'event.category' },
|
||||
{ allowed: true, anonymized: false, field: 'event.action', id: 'event.action' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('updates the allowReplacement list when update is `allowReplacement` and the operation is `remove`', () => {
|
||||
|
@ -195,7 +314,16 @@ describe('helpers', () => {
|
|||
selectedPromptContext,
|
||||
update: 'allowReplacement',
|
||||
});
|
||||
expect(result.allowReplacement).toEqual([]);
|
||||
expect(result.contextAnonymizationFields).toEqual({
|
||||
data: [
|
||||
{ allowed: false, anonymized: true, field: 'event.category', id: 'event.category' },
|
||||
{ allowed: true, anonymized: false, field: 'event.action', id: 'event.action' },
|
||||
{ allowed: true, anonymized: false, field: 'user.name', id: 'user.name' },
|
||||
],
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
total: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not update selectedPromptContext when update is not "allow" or "allowReplacement"', () => {
|
||||
|
@ -209,96 +337,4 @@ describe('helpers', () => {
|
|||
expect(result).toEqual(selectedPromptContext);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDefaultList', () => {
|
||||
it('updates the `defaultAllow` list to add a field when the operation is add', () => {
|
||||
const currentList = ['test1', 'test2'];
|
||||
const setDefaultList = jest.fn();
|
||||
const update = 'defaultAllow';
|
||||
const updates: BatchUpdateListItem[] = [{ field: 'test3', operation: 'add', update }];
|
||||
|
||||
updateDefaultList({ currentList, setDefaultList, update, updates });
|
||||
|
||||
expect(setDefaultList).toBeCalledWith([...currentList, 'test3']);
|
||||
});
|
||||
|
||||
it('updates the `defaultAllow` list to remove a field when the operation is remove', () => {
|
||||
const currentList = ['test1', 'test2'];
|
||||
const setDefaultList = jest.fn();
|
||||
const update = 'defaultAllow';
|
||||
const updates: BatchUpdateListItem[] = [{ field: 'test1', operation: 'remove', update }];
|
||||
|
||||
updateDefaultList({ currentList, setDefaultList, update, updates });
|
||||
|
||||
expect(setDefaultList).toBeCalledWith(['test2']);
|
||||
});
|
||||
|
||||
it('does NOT invoke `setDefaultList` when `update` does NOT match any of the batched `updates` types', () => {
|
||||
const currentList = ['test1', 'test2'];
|
||||
const setDefaultList = jest.fn();
|
||||
const update = 'allow';
|
||||
const updates: BatchUpdateListItem[] = [
|
||||
{ field: 'test1', operation: 'remove', update: 'defaultAllow' }, // update does not match
|
||||
];
|
||||
|
||||
updateDefaultList({ currentList, setDefaultList, update, updates });
|
||||
|
||||
expect(setDefaultList).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('does NOT invoke `setDefaultList` when `updates` is empty', () => {
|
||||
const currentList = ['test1', 'test2'];
|
||||
const setDefaultList = jest.fn();
|
||||
const update = 'defaultAllow';
|
||||
const updates: BatchUpdateListItem[] = []; // no updates
|
||||
|
||||
updateDefaultList({ currentList, setDefaultList, update, updates });
|
||||
|
||||
expect(setDefaultList).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDefaults', () => {
|
||||
const setDefaultAllow = jest.fn();
|
||||
const setDefaultAllowReplacement = jest.fn();
|
||||
|
||||
const defaultAllow = ['field1', 'field2'];
|
||||
const defaultAllowReplacement = ['field2'];
|
||||
const batchUpdateListItems: BatchUpdateListItem[] = [
|
||||
{
|
||||
field: 'field1',
|
||||
operation: 'remove',
|
||||
update: 'defaultAllow',
|
||||
},
|
||||
{
|
||||
field: 'host.name',
|
||||
operation: 'add',
|
||||
update: 'defaultAllowReplacement',
|
||||
},
|
||||
];
|
||||
|
||||
it('updates defaultAllow with filtered updates', () => {
|
||||
updateDefaults({
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
updates: batchUpdateListItems,
|
||||
});
|
||||
|
||||
expect(setDefaultAllow).toHaveBeenCalledWith(['field2']);
|
||||
});
|
||||
|
||||
it('updates defaultAllowReplacement with filtered updates', () => {
|
||||
updateDefaults({
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
updates: batchUpdateListItems,
|
||||
});
|
||||
|
||||
expect(setDefaultAllowReplacement).toHaveBeenCalledWith(['field2', 'host.name']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SelectedPromptContext } from '../../assistant/prompt_context/types';
|
||||
import type { BatchUpdateListItem } from '../context_editor/types';
|
||||
|
||||
export const getIsDataAnonymizable = (rawData: string | Record<string, string[]>): boolean =>
|
||||
typeof rawData !== 'string';
|
||||
|
@ -18,36 +16,6 @@ export interface Stats {
|
|||
total: number;
|
||||
}
|
||||
|
||||
export const isAllowed = ({ allowSet, field }: { allowSet: Set<string>; field: string }): boolean =>
|
||||
allowSet.has(field);
|
||||
|
||||
export const isDenied = ({ allowSet, field }: { allowSet: Set<string>; field: string }): boolean =>
|
||||
!allowSet.has(field);
|
||||
|
||||
export const isAnonymized = ({
|
||||
allowReplacementSet,
|
||||
field,
|
||||
}: {
|
||||
allowReplacementSet: Set<string>;
|
||||
field: string;
|
||||
}): boolean => allowReplacementSet.has(field);
|
||||
|
||||
export const updateList = ({
|
||||
field,
|
||||
list,
|
||||
operation,
|
||||
}: {
|
||||
field: string;
|
||||
list: string[];
|
||||
operation: 'add' | 'remove';
|
||||
}): string[] => {
|
||||
if (operation === 'add') {
|
||||
return list.includes(field) ? list : [...list, field];
|
||||
} else {
|
||||
return list.filter((x) => x !== field);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSelectedPromptContext = ({
|
||||
field,
|
||||
operation,
|
||||
|
@ -65,71 +33,45 @@ export const updateSelectedPromptContext = ({
|
|||
| 'deny'
|
||||
| 'denyReplacement';
|
||||
}): SelectedPromptContext => {
|
||||
const { allow, allowReplacement } = selectedPromptContext;
|
||||
const { contextAnonymizationFields } = selectedPromptContext;
|
||||
if (!contextAnonymizationFields) {
|
||||
return selectedPromptContext;
|
||||
}
|
||||
|
||||
switch (update) {
|
||||
case 'allow':
|
||||
return {
|
||||
...selectedPromptContext,
|
||||
allow: updateList({ field, list: allow, operation }),
|
||||
contextAnonymizationFields: {
|
||||
...contextAnonymizationFields,
|
||||
data: [
|
||||
...contextAnonymizationFields.data.filter((f) => f.field !== field),
|
||||
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
...contextAnonymizationFields.data.find((f) => f.field === field)!,
|
||||
allowed: operation === 'add',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
case 'allowReplacement':
|
||||
return {
|
||||
...selectedPromptContext,
|
||||
allowReplacement: updateList({ field, list: allowReplacement, operation }),
|
||||
contextAnonymizationFields: {
|
||||
...contextAnonymizationFields,
|
||||
data: [
|
||||
...contextAnonymizationFields.data.filter((f) => f.field !== field),
|
||||
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
...contextAnonymizationFields.data.find((f) => f.field === field)!,
|
||||
anonymized: operation === 'add',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
default:
|
||||
return selectedPromptContext;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateDefaultList = ({
|
||||
currentList,
|
||||
setDefaultList,
|
||||
update,
|
||||
updates,
|
||||
}: {
|
||||
currentList: string[];
|
||||
setDefaultList: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
update: 'allow' | 'allowReplacement' | 'defaultAllow' | 'defaultAllowReplacement' | 'deny';
|
||||
updates: BatchUpdateListItem[];
|
||||
}): void => {
|
||||
const filteredUpdates = updates.filter((x) => x.update === update);
|
||||
|
||||
if (filteredUpdates.length > 0) {
|
||||
const updatedList = filteredUpdates.reduce(
|
||||
(acc, { field, operation }) => updateList({ field, list: acc, operation }),
|
||||
currentList
|
||||
);
|
||||
|
||||
setDefaultList(updatedList);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateDefaults = ({
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
updates,
|
||||
}: {
|
||||
defaultAllow: string[];
|
||||
defaultAllowReplacement: string[];
|
||||
setDefaultAllow: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setDefaultAllowReplacement: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
updates: BatchUpdateListItem[];
|
||||
}): void => {
|
||||
updateDefaultList({
|
||||
currentList: defaultAllow,
|
||||
setDefaultList: setDefaultAllow,
|
||||
update: 'defaultAllow',
|
||||
updates,
|
||||
});
|
||||
|
||||
updateDefaultList({
|
||||
currentList: defaultAllowReplacement,
|
||||
setDefaultList: setDefaultAllowReplacement,
|
||||
update: 'defaultAllowReplacement',
|
||||
updates,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -15,8 +15,25 @@ import { DataAnonymizationEditor } from '.';
|
|||
|
||||
describe('DataAnonymizationEditor', () => {
|
||||
const mockSelectedPromptContext: SelectedPromptContext = {
|
||||
allow: ['field1', 'field2'],
|
||||
allowReplacement: ['field1'],
|
||||
contextAnonymizationFields: {
|
||||
total: 0,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [
|
||||
{
|
||||
id: 'field1',
|
||||
field: 'field1',
|
||||
anonymized: true,
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
id: 'field2',
|
||||
field: 'field2',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
promptContextId: 'test-id',
|
||||
rawData: 'test-raw-data',
|
||||
};
|
||||
|
|
|
@ -10,11 +10,10 @@ import React, { useCallback, useMemo } from 'react';
|
|||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useAssistantContext } from '../assistant_context';
|
||||
import type { SelectedPromptContext } from '../assistant/prompt_context/types';
|
||||
import { ContextEditor } from './context_editor';
|
||||
import { BatchUpdateListItem } from './context_editor/types';
|
||||
import { getIsDataAnonymizable, updateDefaults, updateSelectedPromptContext } from './helpers';
|
||||
import { getIsDataAnonymizable, updateSelectedPromptContext } from './helpers';
|
||||
import { ReadOnlyContextViewer } from './read_only_context_viewer';
|
||||
import { Stats } from './stats';
|
||||
|
||||
|
@ -33,8 +32,6 @@ const DataAnonymizationEditorComponent: React.FC<Props> = ({
|
|||
selectedPromptContext,
|
||||
setSelectedPromptContexts,
|
||||
}) => {
|
||||
const { defaultAllow, defaultAllowReplacement, setDefaultAllow, setDefaultAllowReplacement } =
|
||||
useAssistantContext();
|
||||
const isDataAnonymizable = useMemo<boolean>(
|
||||
() => getIsDataAnonymizable(selectedPromptContext.rawData),
|
||||
[selectedPromptContext]
|
||||
|
@ -57,30 +54,16 @@ const DataAnonymizationEditorComponent: React.FC<Props> = ({
|
|||
...prev,
|
||||
[selectedPromptContext.promptContextId]: updatedPromptContext,
|
||||
}));
|
||||
|
||||
updateDefaults({
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
updates,
|
||||
});
|
||||
},
|
||||
[
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
selectedPromptContext,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
setSelectedPromptContexts,
|
||||
]
|
||||
[selectedPromptContext, setSelectedPromptContexts]
|
||||
);
|
||||
|
||||
return (
|
||||
<EditorContainer data-test-subj="dataAnonymizationEditor">
|
||||
<Stats
|
||||
isDataAnonymizable={isDataAnonymizable}
|
||||
selectedPromptContext={selectedPromptContext}
|
||||
anonymizationFields={selectedPromptContext.contextAnonymizationFields?.data}
|
||||
rawData={selectedPromptContext.rawData}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
@ -89,8 +72,14 @@ const DataAnonymizationEditorComponent: React.FC<Props> = ({
|
|||
<ReadOnlyContextViewer rawData={selectedPromptContext.rawData} />
|
||||
) : (
|
||||
<ContextEditor
|
||||
allow={selectedPromptContext.allow}
|
||||
allowReplacement={selectedPromptContext.allowReplacement}
|
||||
anonymizationFields={
|
||||
selectedPromptContext.contextAnonymizationFields ?? {
|
||||
total: 0,
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
data: [],
|
||||
}
|
||||
}
|
||||
onListUpdated={onListUpdated}
|
||||
rawData={selectedPromptContext.rawData}
|
||||
/>
|
||||
|
|
|
@ -8,26 +8,38 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { SelectedPromptContext } from '../../assistant/prompt_context/types';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { Stats } from '.';
|
||||
|
||||
describe('Stats', () => {
|
||||
const selectedPromptContext: SelectedPromptContext = {
|
||||
allow: ['field1', 'field2'],
|
||||
allowReplacement: ['field1'],
|
||||
promptContextId: 'abcd',
|
||||
rawData: {
|
||||
field1: ['value1', 'value2'],
|
||||
field2: ['value3, value4', 'value5'],
|
||||
field3: ['value6'],
|
||||
const anonymizationFields = [
|
||||
{
|
||||
id: 'field1',
|
||||
field: 'field1',
|
||||
anonymized: true,
|
||||
allowed: true,
|
||||
},
|
||||
{
|
||||
id: 'field2',
|
||||
field: 'field2',
|
||||
anonymized: false,
|
||||
allowed: true,
|
||||
},
|
||||
];
|
||||
const rawData = {
|
||||
field1: ['value1', 'value2'],
|
||||
field2: ['value3, value4', 'value5'],
|
||||
field3: ['value6'],
|
||||
};
|
||||
|
||||
it('renders the expected allowed stat content', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Stats isDataAnonymizable={true} selectedPromptContext={selectedPromptContext} />
|
||||
<Stats
|
||||
isDataAnonymizable={true}
|
||||
anonymizationFields={anonymizationFields}
|
||||
rawData={rawData}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -37,7 +49,11 @@ describe('Stats', () => {
|
|||
it('renders the expected anonymized stat content', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Stats isDataAnonymizable={true} selectedPromptContext={selectedPromptContext} />
|
||||
<Stats
|
||||
isDataAnonymizable={true}
|
||||
anonymizationFields={anonymizationFields}
|
||||
rawData={rawData}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -47,7 +63,11 @@ describe('Stats', () => {
|
|||
it('renders the expected available stat content', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Stats isDataAnonymizable={true} selectedPromptContext={selectedPromptContext} />
|
||||
<Stats
|
||||
isDataAnonymizable={true}
|
||||
anonymizationFields={anonymizationFields}
|
||||
rawData={rawData}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -57,7 +77,11 @@ describe('Stats', () => {
|
|||
it('should not display the allowed stat when isDataAnonymizable is false', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Stats isDataAnonymizable={false} selectedPromptContext={selectedPromptContext} />
|
||||
<Stats
|
||||
isDataAnonymizable={false}
|
||||
anonymizationFields={anonymizationFields}
|
||||
rawData={rawData}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -67,7 +91,11 @@ describe('Stats', () => {
|
|||
it('should not display the available stat when isDataAnonymizable is false', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<Stats isDataAnonymizable={false} selectedPromptContext={selectedPromptContext} />
|
||||
<Stats
|
||||
isDataAnonymizable={false}
|
||||
anonymizationFields={anonymizationFields}
|
||||
rawData={rawData}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -10,9 +10,9 @@ import React, { useMemo } from 'react';
|
|||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { AllowedStat } from './allowed_stat';
|
||||
import { AnonymizedStat } from './anonymized_stat';
|
||||
import type { SelectedPromptContext } from '../../assistant/prompt_context/types';
|
||||
import { getStats } from '../get_stats';
|
||||
import { AvailableStat } from './available_stat';
|
||||
|
||||
|
@ -22,13 +22,18 @@ const StatFlexItem = styled(EuiFlexItem)`
|
|||
|
||||
interface Props {
|
||||
isDataAnonymizable: boolean;
|
||||
selectedPromptContext: SelectedPromptContext;
|
||||
anonymizationFields?: AnonymizationFieldResponse[];
|
||||
rawData?: string | Record<string, string[]>;
|
||||
}
|
||||
|
||||
const StatsComponent: React.FC<Props> = ({ isDataAnonymizable, selectedPromptContext }) => {
|
||||
const StatsComponent: React.FC<Props> = ({ isDataAnonymizable, anonymizationFields, rawData }) => {
|
||||
const { allowed, anonymized, total } = useMemo(
|
||||
() => getStats(selectedPromptContext),
|
||||
[selectedPromptContext]
|
||||
() =>
|
||||
getStats({
|
||||
anonymizationFields,
|
||||
rawData,
|
||||
}),
|
||||
[anonymizationFields, rawData]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -69,18 +69,12 @@ export const TestProvidersComponent: React.FC<Props> = ({
|
|||
actionTypeRegistry={actionTypeRegistry}
|
||||
assistantAvailability={assistantAvailability}
|
||||
augmentMessageCodeBlocks={jest.fn().mockReturnValue([])}
|
||||
baseAllow={[]}
|
||||
baseAllowReplacement={[]}
|
||||
basePath={'https://localhost:5601/kbn'}
|
||||
defaultAllow={[]}
|
||||
defaultAllowReplacement={[]}
|
||||
docLinks={{
|
||||
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
|
||||
DOC_LINK_VERSION: 'current',
|
||||
}}
|
||||
getComments={mockGetComments}
|
||||
setDefaultAllow={jest.fn()}
|
||||
setDefaultAllowReplacement={jest.fn()}
|
||||
http={mockHttp}
|
||||
baseConversations={{}}
|
||||
{...providerContext}
|
||||
|
|
|
@ -142,7 +142,7 @@ export type { GetKnowledgeBaseStatusResponse } from './impl/assistant/api';
|
|||
export type { PostKnowledgeBaseResponse } from './impl/assistant/api';
|
||||
|
||||
export { useFetchCurrentUserConversations } from './impl/assistant/api/conversations/use_fetch_current_user_conversations';
|
||||
export * from './impl/assistant/api/conversations/use_bulk_actions_conversations';
|
||||
export * from './impl/assistant/api/conversations/bulk_update_actions_conversations';
|
||||
export { getConversationById } from './impl/assistant/api/conversations/conversations';
|
||||
|
||||
export { mergeBaseWithPersistedConversations } from './impl/assistant/helpers';
|
||||
|
|
|
@ -62,18 +62,12 @@ export const TestProvidersComponent: React.FC<Props> = ({ children, isILMAvailab
|
|||
actionTypeRegistry={actionTypeRegistry}
|
||||
assistantAvailability={mockAssistantAvailability}
|
||||
augmentMessageCodeBlocks={jest.fn()}
|
||||
baseAllow={[]}
|
||||
baseAllowReplacement={[]}
|
||||
basePath={'https://localhost:5601/kbn'}
|
||||
defaultAllow={[]}
|
||||
defaultAllowReplacement={[]}
|
||||
docLinks={{
|
||||
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
|
||||
DOC_LINK_VERSION: 'current',
|
||||
}}
|
||||
getComments={mockGetComments}
|
||||
setDefaultAllow={jest.fn()}
|
||||
setDefaultAllowReplacement={jest.fn()}
|
||||
http={mockHttp}
|
||||
baseConversations={{}}
|
||||
>
|
||||
|
|
|
@ -90,3 +90,16 @@ export const DEFAULT_ALLOW_REPLACEMENT = [
|
|||
'user.domain',
|
||||
'user.name',
|
||||
];
|
||||
|
||||
export const getDefaultAnonymizationFields = (spaceId: string) => {
|
||||
const changedAt = new Date().toISOString();
|
||||
return DEFAULT_ALLOW.map((field) => ({
|
||||
'@timestamp': changedAt,
|
||||
created_at: changedAt,
|
||||
created_by: '',
|
||||
field,
|
||||
anonymized: DEFAULT_ALLOW_REPLACEMENT.includes(field),
|
||||
allowed: true,
|
||||
namespace: spaceId,
|
||||
}));
|
||||
};
|
|
@ -29,3 +29,8 @@ export const PROMPTS_TABLE_MAX_PAGE_SIZE = 100;
|
|||
|
||||
// Capabilities
|
||||
export const CAPABILITIES = `${BASE_PATH}/capabilities`;
|
||||
|
||||
/**
|
||||
Licensing requirements
|
||||
*/
|
||||
export const MINIMUM_AI_ASSISTANT_LICENSE = 'platinum' as const;
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"data",
|
||||
"ml",
|
||||
"taskManager",
|
||||
"licensing",
|
||||
"spaces",
|
||||
"security"
|
||||
]
|
||||
|
|
|
@ -6,16 +6,16 @@
|
|||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { SearchEsAnonymizationFieldsSchema } from '../ai_assistant_data_clients/anonymization_fields/types';
|
||||
import {
|
||||
AnonymizationFieldCreateProps,
|
||||
AnonymizationFieldResponse,
|
||||
AnonymizationFieldUpdateProps,
|
||||
PerformBulkActionRequestBody,
|
||||
} from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { EsAnonymizationFieldsSchema } from '../ai_assistant_data_clients/anonymization_fields/types';
|
||||
|
||||
export const getAnonymizationFieldsSearchEsMock = () => {
|
||||
const searchResponse: estypes.SearchResponse<SearchEsAnonymizationFieldsSchema> = {
|
||||
const searchResponse: estypes.SearchResponse<EsAnonymizationFieldsSchema> = {
|
||||
took: 3,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
|
@ -41,14 +41,9 @@ export const getAnonymizationFieldsSearchEsMock = () => {
|
|||
namespace: 'default',
|
||||
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
|
||||
field: 'testField',
|
||||
default_allow: true,
|
||||
default_allow_replacement: false,
|
||||
allowed: true,
|
||||
anonymized: false,
|
||||
created_by: 'elastic',
|
||||
users: [
|
||||
{
|
||||
name: 'elastic',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -59,15 +54,15 @@ export const getAnonymizationFieldsSearchEsMock = () => {
|
|||
|
||||
export const getCreateAnonymizationFieldSchemaMock = (): AnonymizationFieldCreateProps => ({
|
||||
field: 'testField',
|
||||
defaultAllow: false,
|
||||
defaultAllowReplacement: true,
|
||||
allowed: false,
|
||||
anonymized: true,
|
||||
});
|
||||
|
||||
export const getUpdateAnonymizationFieldSchemaMock = (
|
||||
promptId = 'prompt-1'
|
||||
): AnonymizationFieldUpdateProps => ({
|
||||
defaultAllowReplacement: true,
|
||||
defaultAllow: false,
|
||||
anonymized: true,
|
||||
allowed: false,
|
||||
id: promptId,
|
||||
});
|
||||
|
||||
|
@ -76,16 +71,11 @@ export const getAnonymizationFieldMock = (
|
|||
): AnonymizationFieldResponse => ({
|
||||
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
|
||||
field: 'testField',
|
||||
defaultAllow: false,
|
||||
allowed: false,
|
||||
...params,
|
||||
createdAt: '2019-12-13T16:40:33.400Z',
|
||||
updatedAt: '2019-12-13T16:40:33.400Z',
|
||||
namespace: 'default',
|
||||
users: [
|
||||
{
|
||||
name: 'elastic',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const getQueryAnonymizationFieldParams = (
|
||||
|
@ -94,14 +84,14 @@ export const getQueryAnonymizationFieldParams = (
|
|||
return isUpdate
|
||||
? {
|
||||
field: 'testField',
|
||||
defaultAllowReplacement: true,
|
||||
defaultAllow: false,
|
||||
anonymized: true,
|
||||
allowed: false,
|
||||
id: '1',
|
||||
}
|
||||
: {
|
||||
field: 'test 2',
|
||||
defaultAllowReplacement: true,
|
||||
defaultAllow: false,
|
||||
anonymized: true,
|
||||
allowed: false,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { SearchEsPromptsSchema } from '../ai_assistant_data_clients/prompts/types';
|
||||
import { EsPromptsSchema } from '../ai_assistant_data_clients/prompts/types';
|
||||
import {
|
||||
PerformBulkActionRequestBody,
|
||||
PromptCreateProps,
|
||||
|
@ -15,7 +15,7 @@ import {
|
|||
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
|
||||
|
||||
export const getPromptsSearchEsMock = () => {
|
||||
const searchResponse: estypes.SearchResponse<SearchEsPromptsSchema> = {
|
||||
const searchResponse: estypes.SearchResponse<EsPromptsSchema> = {
|
||||
took: 3,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
|
|
|
@ -61,12 +61,14 @@ const createMockConfig = () => ({});
|
|||
|
||||
const createAppClientMock = () => ({});
|
||||
|
||||
const license = licensingMock.createLicense({ license: { type: 'platinum' } });
|
||||
const createRequestContextMock = (
|
||||
clients: MockClients = createMockClients()
|
||||
): ElasticAssistantRequestHandlerContextMock => {
|
||||
return {
|
||||
core: clients.core,
|
||||
elasticAssistant: createElasticAssistantRequestContextMock(clients),
|
||||
licensing: licensingMock.createRequestHandlerContext({ license }),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -11,9 +11,9 @@ import { estypes } from '@elastic/elasticsearch';
|
|||
import { EsConversationSchema } from '../ai_assistant_data_clients/conversations/types';
|
||||
import { FindResponse } from '../ai_assistant_data_clients/find';
|
||||
import { ConversationResponse } from '@kbn/elastic-assistant-common';
|
||||
import { SearchEsPromptsSchema } from '../ai_assistant_data_clients/prompts/types';
|
||||
import { EsPromptsSchema } from '../ai_assistant_data_clients/prompts/types';
|
||||
import { getPromptsSearchEsMock } from './prompts_schema.mock';
|
||||
import { SearchEsAnonymizationFieldsSchema } from '../ai_assistant_data_clients/anonymization_fields/types';
|
||||
import { EsAnonymizationFieldsSchema } from '../ai_assistant_data_clients/anonymization_fields/types';
|
||||
import { getAnonymizationFieldsSearchEsMock } from './anonymization_fields_schema.mock';
|
||||
|
||||
export const responseMock = {
|
||||
|
@ -34,7 +34,7 @@ export const getFindConversationsResultWithSingleHit = (): FindResponse<EsConver
|
|||
data: getConversationSearchEsMock(),
|
||||
});
|
||||
|
||||
export const getFindPromptsResultWithSingleHit = (): FindResponse<SearchEsPromptsSchema> => ({
|
||||
export const getFindPromptsResultWithSingleHit = (): FindResponse<EsPromptsSchema> => ({
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 1,
|
||||
|
@ -42,7 +42,7 @@ export const getFindPromptsResultWithSingleHit = (): FindResponse<SearchEsPrompt
|
|||
});
|
||||
|
||||
export const getFindAnonymizationFieldsResultWithSingleHit =
|
||||
(): FindResponse<SearchEsAnonymizationFieldsSchema> => ({
|
||||
(): FindResponse<EsAnonymizationFieldsSchema> => ({
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 1,
|
||||
|
|
|
@ -23,12 +23,12 @@ export const assistantAnonymizationFieldsFieldMap: FieldMap = {
|
|||
array: false,
|
||||
required: false,
|
||||
},
|
||||
default_allow: {
|
||||
allowed: {
|
||||
type: 'boolean',
|
||||
array: false,
|
||||
required: false,
|
||||
},
|
||||
default_allow_replacement: {
|
||||
anonymized: {
|
||||
type: 'boolean',
|
||||
array: false,
|
||||
required: false,
|
||||
|
@ -53,19 +53,4 @@ export const assistantAnonymizationFieldsFieldMap: FieldMap = {
|
|||
array: false,
|
||||
required: false,
|
||||
},
|
||||
users: {
|
||||
type: 'nested',
|
||||
array: true,
|
||||
required: false,
|
||||
},
|
||||
'users.id': {
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
required: false,
|
||||
},
|
||||
'users.name': {
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
required: false,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -14,12 +14,31 @@ import {
|
|||
import { AuthenticatedUser } from '@kbn/security-plugin-types-common';
|
||||
import {
|
||||
CreateAnonymizationFieldSchema,
|
||||
SearchEsAnonymizationFieldsSchema,
|
||||
EsAnonymizationFieldsSchema,
|
||||
UpdateAnonymizationFieldSchema,
|
||||
} from './types';
|
||||
|
||||
export const transformESToAnonymizationFields = (
|
||||
response: estypes.SearchResponse<SearchEsAnonymizationFieldsSchema>
|
||||
response: EsAnonymizationFieldsSchema[]
|
||||
): AnonymizationFieldResponse[] => {
|
||||
return response.map((anonymizationFieldSchema) => {
|
||||
const anonymizationField: AnonymizationFieldResponse = {
|
||||
timestamp: anonymizationFieldSchema['@timestamp'],
|
||||
createdAt: anonymizationFieldSchema.created_at,
|
||||
field: anonymizationFieldSchema.field,
|
||||
allowed: anonymizationFieldSchema.allowed,
|
||||
anonymized: anonymizationFieldSchema.anonymized,
|
||||
updatedAt: anonymizationFieldSchema.updated_at,
|
||||
namespace: anonymizationFieldSchema.namespace,
|
||||
id: anonymizationFieldSchema.id,
|
||||
};
|
||||
|
||||
return anonymizationField;
|
||||
});
|
||||
};
|
||||
|
||||
export const transformESSearchToAnonymizationFields = (
|
||||
response: estypes.SearchResponse<EsAnonymizationFieldsSchema>
|
||||
): AnonymizationFieldResponse[] => {
|
||||
return response.hits.hits
|
||||
.filter((hit) => hit._source !== undefined)
|
||||
|
@ -29,14 +48,9 @@ export const transformESToAnonymizationFields = (
|
|||
const anonymizationField: AnonymizationFieldResponse = {
|
||||
timestamp: anonymizationFieldSchema['@timestamp'],
|
||||
createdAt: anonymizationFieldSchema.created_at,
|
||||
users:
|
||||
anonymizationFieldSchema.users?.map((user) => ({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
})) ?? [],
|
||||
field: anonymizationFieldSchema.field,
|
||||
defaultAllow: anonymizationFieldSchema.default_allow,
|
||||
defaultAllowReplacement: anonymizationFieldSchema.default_allow_replacement,
|
||||
allowed: anonymizationFieldSchema.allowed,
|
||||
anonymized: anonymizationFieldSchema.anonymized,
|
||||
updatedAt: anonymizationFieldSchema.updated_at,
|
||||
namespace: anonymizationFieldSchema.namespace,
|
||||
id: hit._id,
|
||||
|
@ -49,39 +63,29 @@ export const transformESToAnonymizationFields = (
|
|||
export const transformToUpdateScheme = (
|
||||
user: AuthenticatedUser,
|
||||
updatedAt: string,
|
||||
{ defaultAllow, defaultAllowReplacement, id }: AnonymizationFieldUpdateProps
|
||||
{ allowed, anonymized, id }: AnonymizationFieldUpdateProps
|
||||
): UpdateAnonymizationFieldSchema => {
|
||||
return {
|
||||
id,
|
||||
users: [
|
||||
{
|
||||
id: user.profile_uid,
|
||||
name: user.username,
|
||||
},
|
||||
],
|
||||
updated_at: updatedAt,
|
||||
default_allow: defaultAllow,
|
||||
default_allow_replacement: defaultAllowReplacement,
|
||||
updated_by: user.username,
|
||||
allowed,
|
||||
anonymized,
|
||||
};
|
||||
};
|
||||
|
||||
export const transformToCreateScheme = (
|
||||
user: AuthenticatedUser,
|
||||
createdAt: string,
|
||||
{ defaultAllow, defaultAllowReplacement, field }: AnonymizationFieldCreateProps
|
||||
{ allowed, anonymized, field }: AnonymizationFieldCreateProps
|
||||
): CreateAnonymizationFieldSchema => {
|
||||
return {
|
||||
updated_at: createdAt,
|
||||
field,
|
||||
users: [
|
||||
{
|
||||
id: user.profile_uid,
|
||||
name: user.username,
|
||||
},
|
||||
],
|
||||
created_at: createdAt,
|
||||
default_allow: defaultAllow,
|
||||
default_allow_replacement: defaultAllowReplacement,
|
||||
created_by: user.username,
|
||||
allowed,
|
||||
anonymized,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -94,11 +98,11 @@ export const getUpdateScript = ({
|
|||
}) => {
|
||||
return {
|
||||
source: `
|
||||
if (params.assignEmpty == true || params.containsKey('default_allow')) {
|
||||
ctx._source.default_allow = params.default_allow;
|
||||
if (params.assignEmpty == true || params.containsKey('allowed')) {
|
||||
ctx._source.allowed = params.allowed;
|
||||
}
|
||||
if (params.assignEmpty == true || params.containsKey('default_allow_replacement')) {
|
||||
ctx._source.default_allow_replacement = params.default_allow_replacement;
|
||||
if (params.assignEmpty == true || params.containsKey('anonymized')) {
|
||||
ctx._source.anonymized = params.anonymized;
|
||||
}
|
||||
ctx._source.updated_at = params.updated_at;
|
||||
`,
|
||||
|
|
|
@ -5,18 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface SearchEsAnonymizationFieldsSchema {
|
||||
export interface EsAnonymizationFieldsSchema {
|
||||
id: string;
|
||||
'@timestamp': string;
|
||||
created_at: string;
|
||||
created_by: string;
|
||||
field: string;
|
||||
default_allow_replacement?: boolean;
|
||||
default_allow?: boolean;
|
||||
users?: Array<{
|
||||
id?: string;
|
||||
name?: string;
|
||||
}>;
|
||||
anonymized?: boolean;
|
||||
allowed?: boolean;
|
||||
updated_at?: string;
|
||||
updated_by?: string;
|
||||
namespace: string;
|
||||
|
@ -25,12 +21,8 @@ export interface SearchEsAnonymizationFieldsSchema {
|
|||
export interface UpdateAnonymizationFieldSchema {
|
||||
id: string;
|
||||
'@timestamp'?: string;
|
||||
default_allow_replacement?: boolean;
|
||||
default_allow?: boolean;
|
||||
users?: Array<{
|
||||
id?: string;
|
||||
name?: string;
|
||||
}>;
|
||||
anonymized?: boolean;
|
||||
allowed?: boolean;
|
||||
updated_at?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
@ -38,12 +30,8 @@ export interface UpdateAnonymizationFieldSchema {
|
|||
export interface CreateAnonymizationFieldSchema {
|
||||
'@timestamp'?: string;
|
||||
field: string;
|
||||
default_allow_replacement?: boolean;
|
||||
default_allow?: boolean;
|
||||
users?: Array<{
|
||||
id?: string;
|
||||
name?: string;
|
||||
}>;
|
||||
anonymized?: boolean;
|
||||
allowed?: boolean;
|
||||
updated_at?: string;
|
||||
updated_by?: string;
|
||||
created_at?: string;
|
||||
|
|
|
@ -12,10 +12,37 @@ import {
|
|||
PromptUpdateProps,
|
||||
} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen';
|
||||
import { AuthenticatedUser } from '@kbn/security-plugin-types-common';
|
||||
import { CreatePromptSchema, SearchEsPromptsSchema, UpdatePromptSchema } from './types';
|
||||
import { CreatePromptSchema, EsPromptsSchema, UpdatePromptSchema } from './types';
|
||||
|
||||
export const transformESToPrompts = (
|
||||
response: estypes.SearchResponse<SearchEsPromptsSchema>
|
||||
export const transformESToPrompts = (response: EsPromptsSchema[]): PromptResponse[] => {
|
||||
return response.map((promptSchema) => {
|
||||
const prompt: PromptResponse = {
|
||||
timestamp: promptSchema['@timestamp'],
|
||||
createdAt: promptSchema.created_at,
|
||||
users:
|
||||
promptSchema.users?.map((user) => ({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
})) ?? [],
|
||||
content: promptSchema.content,
|
||||
isDefault: promptSchema.is_default,
|
||||
isNewConversationDefault: promptSchema.is_new_conversation_default,
|
||||
updatedAt: promptSchema.updated_at,
|
||||
namespace: promptSchema.namespace,
|
||||
id: promptSchema.id,
|
||||
name: promptSchema.name,
|
||||
promptType: promptSchema.prompt_type,
|
||||
isShared: promptSchema.is_shared,
|
||||
createdBy: promptSchema.created_by,
|
||||
updatedBy: promptSchema.updated_by,
|
||||
};
|
||||
|
||||
return prompt;
|
||||
});
|
||||
};
|
||||
|
||||
export const transformESSearchToPrompts = (
|
||||
response: estypes.SearchResponse<EsPromptsSchema>
|
||||
): PromptResponse[] => {
|
||||
return response.hits.hits
|
||||
.filter((hit) => hit._source !== undefined)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface SearchEsPromptsSchema {
|
||||
export interface EsPromptsSchema {
|
||||
id: string;
|
||||
'@timestamp': string;
|
||||
created_at: string;
|
||||
|
|
|
@ -163,6 +163,9 @@ describe('AI Assistant Service', () => {
|
|||
(AIAssistantConversationsDataClient as jest.Mock).mockImplementation(
|
||||
() => conversationsDataClient
|
||||
);
|
||||
(clusterClient.search as unknown as jest.Mock).mockResolvedValue({
|
||||
hits: { hits: [], total: { value: 0 } },
|
||||
});
|
||||
});
|
||||
|
||||
test('should create new AIAssistantConversationsDataClient', async () => {
|
||||
|
@ -325,6 +328,7 @@ describe('AI Assistant Service', () => {
|
|||
mappings: {},
|
||||
},
|
||||
}));
|
||||
|
||||
clusterClient.indices.simulateIndexTemplate.mockImplementationOnce(async () => ({
|
||||
...SimulateTemplateResponse,
|
||||
template: {
|
||||
|
@ -775,6 +779,9 @@ describe('AI Assistant Service', () => {
|
|||
.mockRejectedValueOnce(new EsErrors.ConnectionError('foo'))
|
||||
.mockRejectedValueOnce(new EsErrors.TimeoutError('timeout'))
|
||||
.mockResolvedValue({ acknowledged: true });
|
||||
(clusterClient.search as unknown as jest.Mock).mockResolvedValue({
|
||||
hits: { hits: [], total: { value: 0 } },
|
||||
});
|
||||
|
||||
const assistantService = new AIAssistantService({
|
||||
logger,
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { Logger, ElasticsearchClient } from '@kbn/core/server';
|
|||
import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server';
|
||||
import { AuthenticatedUser } from '@kbn/security-plugin/server';
|
||||
import { Subject } from 'rxjs';
|
||||
import { getDefaultAnonymizationFields } from '../../common/anonymization';
|
||||
import { AssistantResourceNames } from '../types';
|
||||
import { AIAssistantConversationsDataClient } from '../ai_assistant_data_clients/conversations';
|
||||
import {
|
||||
|
@ -300,24 +301,24 @@ export class AIAssistantService {
|
|||
) {
|
||||
try {
|
||||
this.options.logger.debug(`Initializing spaceId level resources for AIAssistantService`);
|
||||
let conversationsIndexName = await this.conversationsDataStream.getInstalledSpaceName(
|
||||
const conversationsIndexName = await this.conversationsDataStream.getInstalledSpaceName(
|
||||
spaceId
|
||||
);
|
||||
if (!conversationsIndexName) {
|
||||
conversationsIndexName = await this.conversationsDataStream.installSpace(spaceId);
|
||||
await this.conversationsDataStream.installSpace(spaceId);
|
||||
}
|
||||
|
||||
let promptsIndexName = await this.promptsDataStream.getInstalledSpaceName(spaceId);
|
||||
const promptsIndexName = await this.promptsDataStream.getInstalledSpaceName(spaceId);
|
||||
if (!promptsIndexName) {
|
||||
promptsIndexName = await this.promptsDataStream.installSpace(spaceId);
|
||||
await this.promptsDataStream.installSpace(spaceId);
|
||||
}
|
||||
|
||||
let anonymizationFieldsIndexName =
|
||||
const anonymizationFieldsIndexName =
|
||||
await this.anonymizationFieldsDataStream.getInstalledSpaceName(spaceId);
|
||||
|
||||
if (!anonymizationFieldsIndexName) {
|
||||
anonymizationFieldsIndexName = await this.anonymizationFieldsDataStream.installSpace(
|
||||
spaceId
|
||||
);
|
||||
await this.anonymizationFieldsDataStream.installSpace(spaceId);
|
||||
this.createDefaultAnonymizationFields(spaceId);
|
||||
}
|
||||
} catch (error) {
|
||||
this.options.logger.error(
|
||||
|
@ -326,4 +327,31 @@ export class AIAssistantService {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async createDefaultAnonymizationFields(spaceId: string) {
|
||||
const dataClient = new AIAssistantDataClient({
|
||||
logger: this.options.logger,
|
||||
elasticsearchClientPromise: this.options.elasticsearchClientPromise,
|
||||
spaceId,
|
||||
kibanaVersion: this.options.kibanaVersion,
|
||||
indexPatternsResorceName: this.resourceNames.aliases.anonymizationFields,
|
||||
currentUser: null,
|
||||
});
|
||||
|
||||
const existingAnonymizationFields = await (
|
||||
await dataClient?.getReader()
|
||||
).search({
|
||||
body: {
|
||||
size: 1,
|
||||
},
|
||||
allow_no_indices: true,
|
||||
});
|
||||
if (existingAnonymizationFields.hits.total.value === 0) {
|
||||
const writer = await dataClient?.getWriter();
|
||||
const res = await writer?.bulk({
|
||||
documentsToCreate: getDefaultAnonymizationFields(spaceId),
|
||||
});
|
||||
this.options.logger.info(`Created default anonymization fields: ${res?.docs_created.length}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ interface BulkParams<TUpdateParams extends { id: string }, TCreateParams> {
|
|||
documentsToCreate?: TCreateParams[];
|
||||
documentsToUpdate?: TUpdateParams[];
|
||||
documentsToDelete?: string[];
|
||||
getUpdateScript: (document: TUpdateParams, updatedAt: string) => Script;
|
||||
getUpdateScript?: (document: TUpdateParams, updatedAt: string) => Script;
|
||||
authenticatedUser?: AuthenticatedUser;
|
||||
}
|
||||
|
||||
|
@ -106,14 +106,19 @@ export class DocumentsDataWriter implements DocumentsDataWriter {
|
|||
}
|
||||
};
|
||||
|
||||
private getUpdateDocumentsQuery = async <TUpdateParams extends { id: string }>(
|
||||
documentsToUpdate: TUpdateParams[],
|
||||
getUpdateScript: (document: TUpdateParams, updatedAt: string) => Script,
|
||||
authenticatedUser?: AuthenticatedUser
|
||||
) => {
|
||||
const updatedAt = new Date().toISOString();
|
||||
const filterByUser = authenticatedUser
|
||||
? [
|
||||
getFilterByUser = (authenticatedUser: AuthenticatedUser) => ({
|
||||
filter: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'users',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
nested: {
|
||||
path: 'users',
|
||||
|
@ -130,8 +135,17 @@ export class DocumentsDataWriter implements DocumentsDataWriter {
|
|||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
private getUpdateDocumentsQuery = async <TUpdateParams extends { id: string }>(
|
||||
documentsToUpdate: TUpdateParams[],
|
||||
getUpdateScript: (document: TUpdateParams, updatedAt: string) => Script,
|
||||
authenticatedUser?: AuthenticatedUser
|
||||
) => {
|
||||
const updatedAt = new Date().toISOString();
|
||||
|
||||
const responseToUpdate = await this.options.esClient.search({
|
||||
body: {
|
||||
|
@ -149,8 +163,8 @@ export class DocumentsDataWriter implements DocumentsDataWriter {
|
|||
],
|
||||
},
|
||||
},
|
||||
...filterByUser,
|
||||
],
|
||||
...(authenticatedUser ? this.getFilterByUser(authenticatedUser) : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -184,27 +198,6 @@ export class DocumentsDataWriter implements DocumentsDataWriter {
|
|||
documentsToDelete: string[],
|
||||
authenticatedUser?: AuthenticatedUser
|
||||
) => {
|
||||
const filterByUser = authenticatedUser
|
||||
? [
|
||||
{
|
||||
nested: {
|
||||
path: 'users',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: authenticatedUser.profile_uid
|
||||
? { 'users.id': authenticatedUser.profile_uid }
|
||||
: { 'users.name': authenticatedUser.username },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const responseToDelete = await this.options.esClient.search({
|
||||
body: {
|
||||
query: {
|
||||
|
@ -221,8 +214,8 @@ export class DocumentsDataWriter implements DocumentsDataWriter {
|
|||
],
|
||||
},
|
||||
},
|
||||
...filterByUser,
|
||||
],
|
||||
...(authenticatedUser ? this.getFilterByUser(authenticatedUser) : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -246,13 +239,12 @@ export class DocumentsDataWriter implements DocumentsDataWriter {
|
|||
private buildBulkOperations = async <TUpdateParams extends { id: string }, TCreateParams>(
|
||||
params: BulkParams<TUpdateParams, TCreateParams>
|
||||
): Promise<BulkOperationContainer[]> => {
|
||||
const documentCreateBody =
|
||||
params.authenticatedUser && params.documentsToCreate
|
||||
? params.documentsToCreate.flatMap((document) => [
|
||||
{ create: { _index: this.options.index, _id: uuidV4() } },
|
||||
document,
|
||||
])
|
||||
: [];
|
||||
const documentCreateBody = params.documentsToCreate
|
||||
? params.documentsToCreate.flatMap((document) => [
|
||||
{ create: { _index: this.options.index, _id: uuidV4() } },
|
||||
document,
|
||||
])
|
||||
: [];
|
||||
|
||||
const documentDeletedBody =
|
||||
params.documentsToDelete && params.documentsToDelete.length > 0
|
||||
|
@ -260,7 +252,7 @@ export class DocumentsDataWriter implements DocumentsDataWriter {
|
|||
: [];
|
||||
|
||||
const documentUpdatedBody =
|
||||
params.documentsToUpdate && params.documentsToUpdate.length > 0
|
||||
params.documentsToUpdate && params.documentsToUpdate.length > 0 && params.getUpdateScript
|
||||
? await this.getUpdateDocumentsQuery(
|
||||
params.documentsToUpdate,
|
||||
params.getUpdateScript,
|
||||
|
|
|
@ -30,8 +30,7 @@ export const callAgentExecutor: AgentExecutor<true | false> = async ({
|
|||
abortSignal,
|
||||
actions,
|
||||
alertsIndexPattern,
|
||||
allow,
|
||||
allowReplacement,
|
||||
anonymizationFields,
|
||||
isEnabledKnowledgeBase,
|
||||
assistantTools = [],
|
||||
connectorId,
|
||||
|
@ -96,8 +95,7 @@ export const callAgentExecutor: AgentExecutor<true | false> = async ({
|
|||
|
||||
// Fetch any applicable tools that the source plugin may have registered
|
||||
const assistantToolParams: AssistantToolParams = {
|
||||
allow,
|
||||
allowReplacement,
|
||||
anonymizationFields,
|
||||
alertsIndexPattern,
|
||||
isEnabledKnowledgeBase,
|
||||
chain,
|
||||
|
|
|
@ -14,6 +14,7 @@ import type { LangChainTracer } from '@langchain/core/tracers/tracer_langchain';
|
|||
import type { AnalyticsServiceSetup } from '@kbn/core-analytics-server';
|
||||
import { ExecuteConnectorRequestBody, Message, Replacements } from '@kbn/elastic-assistant-common';
|
||||
import { StreamFactoryReturnType } from '@kbn/ml-response-stream/server';
|
||||
import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
|
||||
import { ResponseBody } from '../types';
|
||||
import type { AssistantTool } from '../../../types';
|
||||
|
||||
|
@ -21,8 +22,7 @@ export interface AgentExecutorParams<T extends boolean> {
|
|||
abortSignal?: AbortSignal;
|
||||
alertsIndexPattern?: string;
|
||||
actions: ActionsPluginStart;
|
||||
allow?: string[];
|
||||
allowReplacement?: string[];
|
||||
anonymizationFields?: AnonymizationFieldResponse[];
|
||||
isEnabledKnowledgeBase: boolean;
|
||||
assistantTools?: AssistantTool[];
|
||||
connectorId: string;
|
||||
|
|
|
@ -100,8 +100,6 @@ describe('helpers', () => {
|
|||
it('returns true if the request has valid anonymization params', () => {
|
||||
const request = {
|
||||
body: {
|
||||
allow: ['a', 'b', 'c'],
|
||||
allowReplacement: ['b', 'c'],
|
||||
replacements: { key: 'value' },
|
||||
},
|
||||
} as unknown as KibanaRequest<unknown, unknown, ExecuteConnectorRequestBody>;
|
||||
|
@ -111,81 +109,9 @@ describe('helpers', () => {
|
|||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if allow is undefined', () => {
|
||||
const request = {
|
||||
body: {
|
||||
// allow is undefined
|
||||
allowReplacement: ['b', 'c'],
|
||||
replacements: { key: 'value' },
|
||||
},
|
||||
} as unknown as KibanaRequest<unknown, unknown, ExecuteConnectorRequestBody>;
|
||||
|
||||
const result = requestHasRequiredAnonymizationParams(request);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if allow is empty', () => {
|
||||
const request = {
|
||||
body: {
|
||||
allow: [], // <-- empty
|
||||
allowReplacement: ['b', 'c'],
|
||||
replacements: { key: 'value' },
|
||||
},
|
||||
} as unknown as KibanaRequest<unknown, unknown, ExecuteConnectorRequestBody>;
|
||||
|
||||
const result = requestHasRequiredAnonymizationParams(request);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if allow has non-string values', () => {
|
||||
const request = {
|
||||
body: {
|
||||
allow: ['a', 9876, 'c'], // <-- non-string value
|
||||
allowReplacement: ['b', 'c'],
|
||||
replacements: { key: 'value' },
|
||||
},
|
||||
} as unknown as KibanaRequest<unknown, unknown, ExecuteConnectorRequestBody>;
|
||||
|
||||
const result = requestHasRequiredAnonymizationParams(request);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if allowReplacement is empty', () => {
|
||||
const request = {
|
||||
body: {
|
||||
allow: ['a', 'b', 'c'],
|
||||
allowReplacement: [],
|
||||
replacements: { key: 'value' },
|
||||
},
|
||||
} as unknown as KibanaRequest<unknown, unknown, ExecuteConnectorRequestBody>;
|
||||
|
||||
const result = requestHasRequiredAnonymizationParams(request);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if allowReplacement has non-string values', () => {
|
||||
const request = {
|
||||
body: {
|
||||
allow: ['a', 'b', 'c'],
|
||||
allowReplacement: ['b', 12345], // <-- non-string value
|
||||
replacements: { key: 'value' },
|
||||
},
|
||||
} as unknown as KibanaRequest<unknown, unknown, ExecuteConnectorRequestBody>;
|
||||
|
||||
const result = requestHasRequiredAnonymizationParams(request);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if replacements is empty', () => {
|
||||
const request = {
|
||||
body: {
|
||||
allow: ['a', 'b', 'c'],
|
||||
allowReplacement: ['b', 'c'],
|
||||
replacements: {},
|
||||
},
|
||||
} as unknown as KibanaRequest<unknown, unknown, ExecuteConnectorRequestBody>;
|
||||
|
@ -198,8 +124,6 @@ describe('helpers', () => {
|
|||
it('returns false if replacements has non-string values', () => {
|
||||
const request = {
|
||||
body: {
|
||||
allow: ['a', 'b', 'c'],
|
||||
allowReplacement: ['b', 'c'],
|
||||
replacements: { key: 76543 }, // <-- non-string value
|
||||
},
|
||||
} as unknown as KibanaRequest<unknown, unknown, ExecuteConnectorRequestBody>;
|
||||
|
|
|
@ -33,15 +33,7 @@ export const getLangChainMessages = (
|
|||
export const requestHasRequiredAnonymizationParams = (
|
||||
request: KibanaRequest<unknown, unknown, ExecuteConnectorRequestBody>
|
||||
): boolean => {
|
||||
const { allow, allowReplacement, replacements } = request?.body ?? {};
|
||||
|
||||
const allowIsValid =
|
||||
Array.isArray(allow) &&
|
||||
allow.length > 0 && // at least one field must be in the allow list
|
||||
allow.every((item) => typeof item === 'string');
|
||||
|
||||
const allowReplacementIsValid =
|
||||
Array.isArray(allowReplacement) && allowReplacement.every((item) => typeof item === 'string');
|
||||
const { replacements } = request?.body ?? {};
|
||||
|
||||
const replacementsIsValid =
|
||||
typeof replacements === 'object' &&
|
||||
|
@ -49,5 +41,5 @@ export const requestHasRequiredAnonymizationParams = (
|
|||
(key) => typeof key === 'string' && typeof replacements[key] === 'string'
|
||||
);
|
||||
|
||||
return allowIsValid && allowReplacementIsValid && replacementsIsValid;
|
||||
return replacementsIsValid;
|
||||
};
|
||||
|
|
|
@ -74,14 +74,14 @@ describe('Perform bulk action route', () => {
|
|||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
success: true,
|
||||
anonymization_fields_count: 2,
|
||||
anonymization_fields_count: 3,
|
||||
attributes: {
|
||||
results: someBulkActionResults(),
|
||||
summary: {
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
succeeded: 2,
|
||||
total: 2,
|
||||
succeeded: 3,
|
||||
total: 3,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -94,7 +94,7 @@ describe('Perform bulk action route', () => {
|
|||
(await clients.elasticAssistant.getAIAssistantAnonymizationFieldsDataClient.getWriter())
|
||||
.bulk as jest.Mock
|
||||
).mockResolvedValue({
|
||||
docs_created: [mockAnonymizationField, mockAnonymizationField],
|
||||
docs_created: [mockAnonymizationField],
|
||||
docs_updated: [],
|
||||
docs_deleted: [],
|
||||
errors: [
|
||||
|
@ -111,7 +111,7 @@ describe('Perform bulk action route', () => {
|
|||
document: { id: 'failed-anonymization-field-id-3', name: 'Detect Root/Admin Users' },
|
||||
},
|
||||
],
|
||||
total: 5,
|
||||
total: 4,
|
||||
});
|
||||
clients.elasticAssistant.getAIAssistantAnonymizationFieldsDataClient.findDocuments.mockResolvedValueOnce(
|
||||
Promise.resolve(getEmptyFindResult())
|
||||
|
@ -130,9 +130,9 @@ describe('Perform bulk action route', () => {
|
|||
attributes: {
|
||||
summary: {
|
||||
failed: 3,
|
||||
succeeded: 2,
|
||||
succeeded: 1,
|
||||
skipped: 0,
|
||||
total: 5,
|
||||
total: 4,
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
|
|
|
@ -10,8 +10,8 @@ import type { IKibanaResponse, KibanaResponseFactory, Logger } from '@kbn/core/s
|
|||
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import {
|
||||
API_VERSIONS,
|
||||
ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_BULK_ACTION,
|
||||
ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
|
||||
import {
|
||||
|
@ -29,14 +29,16 @@ import { ElasticAssistantPluginRouter } from '../../types';
|
|||
import { buildResponse } from '../utils';
|
||||
import {
|
||||
getUpdateScript,
|
||||
transformESSearchToAnonymizationFields,
|
||||
transformESToAnonymizationFields,
|
||||
transformToCreateScheme,
|
||||
transformToUpdateScheme,
|
||||
} from '../../ai_assistant_data_clients/anonymization_fields/helpers';
|
||||
import {
|
||||
SearchEsAnonymizationFieldsSchema,
|
||||
EsAnonymizationFieldsSchema,
|
||||
UpdateAnonymizationFieldSchema,
|
||||
} from '../../ai_assistant_data_clients/anonymization_fields/types';
|
||||
import { UPGRADE_LICENSE_MESSAGE, hasAIAssistantLicense } from '../helpers';
|
||||
|
||||
export interface BulkOperationError {
|
||||
message: string;
|
||||
|
@ -127,7 +129,7 @@ export const bulkActionAnonymizationFieldsRoute = (
|
|||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
version: API_VERSIONS.public.v1,
|
||||
validate: {
|
||||
request: {
|
||||
body: buildRouteValidationWithZod(PerformBulkActionRequestBody),
|
||||
|
@ -155,7 +157,15 @@ export const bulkActionAnonymizationFieldsRoute = (
|
|||
// when route is finished by timeout, aborted$ is not getting fired
|
||||
request.events.completed$.subscribe(() => abortController.abort());
|
||||
try {
|
||||
const ctx = await context.resolve(['core', 'elasticAssistant']);
|
||||
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
|
||||
const license = ctx.licensing.license;
|
||||
if (!hasAIAssistantLicense(license)) {
|
||||
return response.forbidden({
|
||||
body: {
|
||||
message: UPGRADE_LICENSE_MESSAGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const authenticatedUser = ctx.elasticAssistant.getCurrentUser();
|
||||
if (authenticatedUser == null) {
|
||||
|
@ -168,18 +178,16 @@ export const bulkActionAnonymizationFieldsRoute = (
|
|||
await ctx.elasticAssistant.getAIAssistantAnonymizationFieldsDataClient();
|
||||
|
||||
if (body.create && body.create.length > 0) {
|
||||
const result = await dataClient?.findDocuments<SearchEsAnonymizationFieldsSchema>({
|
||||
const result = await dataClient?.findDocuments<EsAnonymizationFieldsSchema>({
|
||||
perPage: 100,
|
||||
page: 1,
|
||||
filter: `users:{ id: "${authenticatedUser?.profile_uid}" } AND (${body.create
|
||||
.map((c) => `field:${c.field}`)
|
||||
.join(' OR ')})`,
|
||||
filter: `(${body.create.map((c) => `field:${c.field}`).join(' OR ')})`,
|
||||
fields: ['field'],
|
||||
});
|
||||
if (result?.data != null && result.total > 0) {
|
||||
return assistantResponse.error({
|
||||
statusCode: 409,
|
||||
body: `anonymization for field: "${result.data.hits.hits
|
||||
body: `anonymization field: "${result.data.hits.hits
|
||||
.map((c) => c._id)
|
||||
.join(',')}" already exists`,
|
||||
});
|
||||
|
@ -204,25 +212,21 @@ export const bulkActionAnonymizationFieldsRoute = (
|
|||
),
|
||||
getUpdateScript: (document: UpdateAnonymizationFieldSchema) =>
|
||||
getUpdateScript({ anonymizationField: document, isPatch: true }),
|
||||
authenticatedUser,
|
||||
});
|
||||
|
||||
const created = await dataClient?.findDocuments<SearchEsAnonymizationFieldsSchema>({
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
filter: docsCreated.map((c) => `id:${c}`).join(' OR '),
|
||||
fields: ['id'],
|
||||
});
|
||||
const updated = await dataClient?.findDocuments<SearchEsAnonymizationFieldsSchema>({
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
filter: docsUpdated.map((c) => `id:${c}`).join(' OR '),
|
||||
fields: ['id'],
|
||||
});
|
||||
const created =
|
||||
docsCreated.length > 0
|
||||
? await dataClient?.findDocuments<EsAnonymizationFieldsSchema>({
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
filter: docsCreated.map((c) => `_id:${c}`).join(' OR '),
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return buildBulkResponse(response, {
|
||||
updated: updated?.data ? transformESToAnonymizationFields(updated.data) : [],
|
||||
created: created?.data ? transformESToAnonymizationFields(created.data) : [],
|
||||
updated: docsUpdated
|
||||
? transformESToAnonymizationFields(docsUpdated as EsAnonymizationFieldsSchema[])
|
||||
: [],
|
||||
created: created?.data ? transformESSearchToAnonymizationFields(created?.data) : [],
|
||||
deleted: docsDeleted ?? [],
|
||||
errors,
|
||||
});
|
||||
|
|
|
@ -93,7 +93,7 @@ describe('Find user anonymization fields route', () => {
|
|||
const result = server.validate(request);
|
||||
|
||||
expect(result.badRequest).toHaveBeenCalledWith(
|
||||
`sort_field: Invalid enum value. Expected 'created_at' | 'is_default' | 'title' | 'updated_at', received 'name'`
|
||||
`sort_field: Invalid enum value. Expected 'created_at' | 'anonymized' | 'allowed' | 'field' | 'updated_at', received 'name'`
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server';
|
|||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import {
|
||||
API_VERSIONS,
|
||||
ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_FIND,
|
||||
ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
|
||||
import {
|
||||
|
@ -20,8 +20,9 @@ import {
|
|||
import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common';
|
||||
import { ElasticAssistantPluginRouter } from '../../types';
|
||||
import { buildResponse } from '../utils';
|
||||
import { SearchEsAnonymizationFieldsSchema } from '../../ai_assistant_data_clients/anonymization_fields/types';
|
||||
import { transformESToAnonymizationFields } from '../../ai_assistant_data_clients/anonymization_fields/helpers';
|
||||
import { EsAnonymizationFieldsSchema } from '../../ai_assistant_data_clients/anonymization_fields/types';
|
||||
import { transformESSearchToAnonymizationFields } from '../../ai_assistant_data_clients/anonymization_fields/helpers';
|
||||
import { UPGRADE_LICENSE_MESSAGE, hasAIAssistantLicense } from '../helpers';
|
||||
|
||||
export const findAnonymizationFieldsRoute = (
|
||||
router: ElasticAssistantPluginRouter,
|
||||
|
@ -37,7 +38,7 @@ export const findAnonymizationFieldsRoute = (
|
|||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
version: API_VERSIONS.public.v1,
|
||||
validate: {
|
||||
request: {
|
||||
query: buildRouteValidationWithZod(FindAnonymizationFieldsRequestQuery),
|
||||
|
@ -53,11 +54,19 @@ export const findAnonymizationFieldsRoute = (
|
|||
|
||||
try {
|
||||
const { query } = request;
|
||||
const ctx = await context.resolve(['core', 'elasticAssistant']);
|
||||
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
|
||||
const license = ctx.licensing.license;
|
||||
if (!hasAIAssistantLicense(license)) {
|
||||
return response.forbidden({
|
||||
body: {
|
||||
message: UPGRADE_LICENSE_MESSAGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
const dataClient =
|
||||
await ctx.elasticAssistant.getAIAssistantAnonymizationFieldsDataClient();
|
||||
|
||||
const result = await dataClient?.findDocuments<SearchEsAnonymizationFieldsSchema>({
|
||||
const result = await dataClient?.findDocuments<EsAnonymizationFieldsSchema>({
|
||||
perPage: query.per_page,
|
||||
page: query.page,
|
||||
sortField: query.sort_field,
|
||||
|
@ -72,7 +81,7 @@ export const findAnonymizationFieldsRoute = (
|
|||
perPage: result.perPage,
|
||||
page: result.page,
|
||||
total: result.total,
|
||||
data: transformESToAnonymizationFields(result.data),
|
||||
data: transformESSearchToAnonymizationFields(result.data),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import { KibanaRequest } from '@kbn/core-http-server';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { Message, TraceData } from '@kbn/elastic-assistant-common';
|
||||
import { ILicense } from '@kbn/licensing-plugin/server';
|
||||
import { MINIMUM_AI_ASSISTANT_LICENSE } from '../../common/constants';
|
||||
|
||||
interface GetPluginNameFromRequestParams {
|
||||
request: KibanaRequest;
|
||||
|
@ -79,3 +81,9 @@ export const getMessageFromRawResponse = ({
|
|||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const hasAIAssistantLicense = (license: ILicense): boolean =>
|
||||
license.hasAtLeast(MINIMUM_AI_ASSISTANT_LICENSE);
|
||||
|
||||
export const UPGRADE_LICENSE_MESSAGE =
|
||||
'Your license does not support AI Assistant. Please upgrade your license.';
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
import { PassThrough } from 'stream';
|
||||
import { getConversationResponseMock } from '../ai_assistant_data_clients/conversations/update_conversation.test';
|
||||
import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock';
|
||||
import { getFindAnonymizationFieldsResultWithSingleHit } from '../__mocks__/response';
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
jest.mock('../lib/build_response', () => ({
|
||||
|
@ -112,6 +113,9 @@ const mockContext = {
|
|||
appendConversationMessages:
|
||||
appendConversationMessages.mockResolvedValue(existingConversation),
|
||||
}),
|
||||
getAIAssistantAnonymizationFieldsDataClient: jest.fn().mockResolvedValue({
|
||||
findDocuments: jest.fn().mockResolvedValue(getFindAnonymizationFieldsResultWithSingleHit()),
|
||||
}),
|
||||
},
|
||||
core: {
|
||||
elasticsearch: {
|
||||
|
@ -287,8 +291,10 @@ describe('postActionsConnectorExecuteRoute', () => {
|
|||
...mockRequest,
|
||||
body: {
|
||||
...mockRequest.body,
|
||||
allow: ['@timestamp'],
|
||||
allowReplacement: ['host.name'],
|
||||
anonymizationFields: [
|
||||
{ id: '@timestamp', field: '@timestamp', allowed: true, anonymized: false },
|
||||
{ id: 'host.name', field: 'host.name', allowed: true, anonymized: true },
|
||||
],
|
||||
replacements: [],
|
||||
isEnabledRAGAlerts: true,
|
||||
},
|
||||
|
@ -323,8 +329,10 @@ describe('postActionsConnectorExecuteRoute', () => {
|
|||
body: {
|
||||
...mockRequest.body,
|
||||
isEnabledKnowledgeBase: false,
|
||||
allow: ['@timestamp'],
|
||||
allowReplacement: ['host.name'],
|
||||
anonymizationFields: [
|
||||
{ id: '@timestamp', field: '@timestamp', allowed: true, anonymized: false },
|
||||
{ id: 'host.name', field: 'host.name', allowed: true, anonymized: true },
|
||||
],
|
||||
replacements: [],
|
||||
isEnabledRAGAlerts: true,
|
||||
},
|
||||
|
@ -456,8 +464,10 @@ describe('postActionsConnectorExecuteRoute', () => {
|
|||
body: {
|
||||
...mockRequest.body,
|
||||
isEnabledKnowledgeBase: false,
|
||||
allow: ['@timestamp'],
|
||||
allowReplacement: ['host.name'],
|
||||
anonymizationFields: [
|
||||
{ id: '@timestamp', field: '@timestamp', allowed: true, anonymized: false },
|
||||
{ id: 'host.name', field: 'host.name', allowed: true, anonymized: true },
|
||||
],
|
||||
replacements: [],
|
||||
isEnabledRAGAlerts: true,
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@ import { StreamFactoryReturnType } from '@kbn/ml-response-stream/server';
|
|||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import {
|
||||
ELASTIC_AI_ASSISTANT_INTERNAL_API_VERSION,
|
||||
API_VERSIONS,
|
||||
ExecuteConnectorRequestBody,
|
||||
Message,
|
||||
Replacements,
|
||||
|
@ -38,6 +38,8 @@ import {
|
|||
getPluginNameFromRequest,
|
||||
} from './helpers';
|
||||
import { getLangSmithTracer } from './evaluate/utils';
|
||||
import { EsAnonymizationFieldsSchema } from '../ai_assistant_data_clients/anonymization_fields/types';
|
||||
import { transformESSearchToAnonymizationFields } from '../ai_assistant_data_clients/anonymization_fields/helpers';
|
||||
|
||||
export const postActionsConnectorExecuteRoute = (
|
||||
router: IRouter<ElasticAssistantRequestHandlerContext>,
|
||||
|
@ -53,7 +55,7 @@ export const postActionsConnectorExecuteRoute = (
|
|||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: ELASTIC_AI_ASSISTANT_INTERNAL_API_VERSION,
|
||||
version: API_VERSIONS.internal.v1,
|
||||
validate: {
|
||||
request: {
|
||||
body: buildRouteValidationWithZod(ExecuteConnectorRequestBody),
|
||||
|
@ -79,7 +81,11 @@ export const postActionsConnectorExecuteRoute = (
|
|||
body: `Authenticated user not found`,
|
||||
});
|
||||
}
|
||||
const dataClient = await assistantContext.getAIAssistantConversationsDataClient();
|
||||
const conversationsDataClient =
|
||||
await assistantContext.getAIAssistantConversationsDataClient();
|
||||
|
||||
const anonymizationFieldsDataClient =
|
||||
await assistantContext.getAIAssistantAnonymizationFieldsDataClient();
|
||||
|
||||
let latestReplacements: Replacements = request.body.replacements;
|
||||
const onNewReplacements = (newReplacements: Replacements) => {
|
||||
|
@ -102,7 +108,7 @@ export const postActionsConnectorExecuteRoute = (
|
|||
}
|
||||
|
||||
if (conversationId) {
|
||||
const conversation = await dataClient?.getConversation({
|
||||
const conversation = await conversationsDataClient?.getConversation({
|
||||
id: conversationId,
|
||||
authenticatedUser,
|
||||
});
|
||||
|
@ -112,14 +118,14 @@ export const postActionsConnectorExecuteRoute = (
|
|||
});
|
||||
}
|
||||
|
||||
// messages are anonymized by dataClient
|
||||
// messages are anonymized by conversationsDataClient
|
||||
prevMessages = conversation?.messages?.map((c) => ({
|
||||
role: c.role,
|
||||
content: c.content,
|
||||
}));
|
||||
|
||||
if (request.body.message) {
|
||||
const res = await dataClient?.appendConversationMessages({
|
||||
const res = await conversationsDataClient?.appendConversationMessages({
|
||||
existingConversation: conversation,
|
||||
messages: [
|
||||
{
|
||||
|
@ -141,7 +147,7 @@ export const postActionsConnectorExecuteRoute = (
|
|||
});
|
||||
}
|
||||
}
|
||||
const updatedConversation = await dataClient?.getConversation({
|
||||
const updatedConversation = await conversationsDataClient?.getConversation({
|
||||
id: conversationId,
|
||||
authenticatedUser,
|
||||
});
|
||||
|
@ -158,7 +164,7 @@ export const postActionsConnectorExecuteRoute = (
|
|||
isError = false
|
||||
): Promise<void> => {
|
||||
if (updatedConversation) {
|
||||
await dataClient?.appendConversationMessages({
|
||||
await conversationsDataClient?.appendConversationMessages({
|
||||
existingConversation: updatedConversation,
|
||||
messages: [
|
||||
getMessageFromRawResponse({
|
||||
|
@ -173,7 +179,7 @@ export const postActionsConnectorExecuteRoute = (
|
|||
});
|
||||
}
|
||||
if (Object.keys(latestReplacements).length > 0) {
|
||||
await dataClient?.updateConversation({
|
||||
await conversationsDataClient?.updateConversation({
|
||||
conversationUpdateProps: {
|
||||
id: conversationId,
|
||||
replacements: latestReplacements,
|
||||
|
@ -245,12 +251,19 @@ export const postActionsConnectorExecuteRoute = (
|
|||
|
||||
const elserId = await getElser(request, (await context.core).savedObjects.getClient());
|
||||
|
||||
const anonymizationFieldsRes =
|
||||
await anonymizationFieldsDataClient?.findDocuments<EsAnonymizationFieldsSchema>({
|
||||
perPage: 1000,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
const result: StreamFactoryReturnType['responseWithHeaders'] | StaticReturnType =
|
||||
await callAgentExecutor({
|
||||
abortSignal,
|
||||
alertsIndexPattern: request.body.alertsIndexPattern,
|
||||
allow: request.body.allow,
|
||||
allowReplacement: request.body.allowReplacement,
|
||||
anonymizationFields: anonymizationFieldsRes
|
||||
? transformESSearchToAnonymizationFields(anonymizationFieldsRes.data)
|
||||
: undefined,
|
||||
actions,
|
||||
isEnabledKnowledgeBase: request.body.isEnabledKnowledgeBase ?? false,
|
||||
assistantTools,
|
||||
|
|
|
@ -70,14 +70,14 @@ describe('Perform bulk action route', () => {
|
|||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
success: true,
|
||||
prompts_count: 2,
|
||||
prompts_count: 3,
|
||||
attributes: {
|
||||
results: someBulkActionResults(),
|
||||
summary: {
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
succeeded: 2,
|
||||
total: 2,
|
||||
succeeded: 3,
|
||||
total: 3,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -90,7 +90,7 @@ describe('Perform bulk action route', () => {
|
|||
(await clients.elasticAssistant.getAIAssistantPromptsDataClient.getWriter())
|
||||
.bulk as jest.Mock
|
||||
).mockResolvedValue({
|
||||
docs_created: [mockPrompt, mockPrompt],
|
||||
docs_created: [mockPrompt],
|
||||
docs_updated: [],
|
||||
docs_deleted: [],
|
||||
errors: [
|
||||
|
@ -107,7 +107,7 @@ describe('Perform bulk action route', () => {
|
|||
document: { id: 'failed-prompt-id-3', name: 'Detect Root/Admin Users' },
|
||||
},
|
||||
],
|
||||
total: 5,
|
||||
total: 4,
|
||||
});
|
||||
clients.elasticAssistant.getAIAssistantPromptsDataClient.findDocuments.mockResolvedValueOnce(
|
||||
Promise.resolve(getEmptyFindResult())
|
||||
|
@ -126,9 +126,9 @@ describe('Perform bulk action route', () => {
|
|||
attributes: {
|
||||
summary: {
|
||||
failed: 3,
|
||||
succeeded: 2,
|
||||
succeeded: 1,
|
||||
skipped: 0,
|
||||
total: 5,
|
||||
total: 4,
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue