- {isDetails && !!fieldMapping ? (
+ {isDetails && !!dataViewField ? (
@@ -68,11 +78,14 @@ export const TableCell: React.FC
= React.memo(
if (columnId === 'value') {
return (
);
}
diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx
index f2829a96b95d3..9dd4305cd4b00 100644
--- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx
+++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.test.tsx
@@ -8,36 +8,33 @@
*/
import React from 'react';
-import { getFieldCellActions, getFieldValueCellActions, TableRow } from './table_cell_actions';
-import { DataViewField } from '@kbn/data-views-plugin/common';
+import { getFieldCellActions, getFieldValueCellActions } from './table_cell_actions';
+import { FieldRow } from './field_row';
+import { buildDataTableRecord } from '@kbn/discover-utils';
+import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub';
+import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
describe('TableActions', () => {
- const rows: TableRow[] = [
- {
- action: {
- onFilter: jest.fn(),
- flattenedField: 'flattenedField',
- onToggleColumn: jest.fn(),
- },
- field: {
- pinned: true,
- onTogglePinned: jest.fn(),
- field: 'message',
- fieldMapping: new DataViewField({
- type: 'keyword',
- name: 'message',
- searchable: true,
- aggregatable: true,
- }),
- fieldType: 'keyword',
- displayName: 'message',
- scripted: false,
- },
- value: {
- ignored: undefined,
- formattedValue: 'test',
- },
- },
+ const rows: FieldRow[] = [
+ new FieldRow({
+ name: 'message',
+ flattenedValue: 'flattenedField',
+ hit: buildDataTableRecord(
+ {
+ _ignored: [],
+ _index: 'test',
+ _id: '1',
+ _source: {
+ message: 'test',
+ },
+ },
+ dataView
+ ),
+ dataView,
+ fieldFormats: {} as FieldFormatsStart,
+ isPinned: false,
+ columnsMeta: undefined,
+ }),
];
const Component = () => Component
;
@@ -52,13 +49,13 @@ describe('TableActions', () => {
describe('getFieldCellActions', () => {
it('should render correctly for undefined functions', () => {
expect(
- getFieldCellActions({ rows, filter: undefined, onToggleColumn: jest.fn() }).map((item) =>
+ getFieldCellActions({ rows, onFilter: undefined, onToggleColumn: jest.fn() }).map((item) =>
item(EuiCellParams)
)
).toMatchSnapshot();
expect(
- getFieldCellActions({ rows, filter: undefined, onToggleColumn: undefined }).map((item) =>
+ getFieldCellActions({ rows, onFilter: undefined, onToggleColumn: undefined }).map((item) =>
item(EuiCellParams)
)
).toMatchSnapshot();
@@ -66,7 +63,7 @@ describe('TableActions', () => {
it('should render the panels correctly for defined onFilter function', () => {
expect(
- getFieldCellActions({ rows, filter: jest.fn(), onToggleColumn: jest.fn() }).map((item) =>
+ getFieldCellActions({ rows, onFilter: jest.fn(), onToggleColumn: jest.fn() }).map((item) =>
item(EuiCellParams)
)
).toMatchSnapshot();
@@ -76,13 +73,13 @@ describe('TableActions', () => {
describe('getFieldValueCellActions', () => {
it('should render correctly for undefined functions', () => {
expect(
- getFieldValueCellActions({ rows, filter: undefined }).map((item) => item(EuiCellParams))
+ getFieldValueCellActions({ rows, onFilter: undefined }).map((item) => item(EuiCellParams))
).toMatchSnapshot();
});
it('should render the panels correctly for defined onFilter function', () => {
expect(
- getFieldValueCellActions({ rows, filter: jest.fn() }).map((item) => item(EuiCellParams))
+ getFieldValueCellActions({ rows, onFilter: jest.fn() }).map((item) => item(EuiCellParams))
).toMatchSnapshot();
});
});
diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx
index 408708f674153..e9c5a70770ca5 100644
--- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx
+++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx
@@ -10,45 +10,36 @@
import React from 'react';
import { EuiDataGridColumnCellActionProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { DocViewFilterFn, FieldRecordLegacy } from '@kbn/unified-doc-viewer/types';
-
-export interface TableRow {
- action: Omit;
- field: {
- pinned: boolean;
- onTogglePinned: (field: string) => void;
- } & FieldRecordLegacy['field'];
- value: FieldRecordLegacy['value'];
-}
+import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
+import { FieldRow } from './field_row';
interface TableActionsProps {
Component: EuiDataGridColumnCellActionProps['Component'];
- row: TableRow | undefined; // as we pass `rows[rowIndex]` it's safer to assume that `row` prop can be undefined
+ row: FieldRow | undefined; // as we pass `rows[rowIndex]` it's safer to assume that `row` prop can be undefined
}
-export function isFilterInOutPairDisabled(row: TableRow | undefined): boolean {
+export function isFilterInOutPairDisabled(
+ row: FieldRow | undefined,
+ onFilter: DocViewFilterFn | undefined
+): boolean {
if (!row) {
return false;
}
- const {
- action: { onFilter },
- field: { fieldMapping },
- value: { ignored },
- } = row;
+ const { dataViewField, ignoredReason } = row;
- return Boolean(onFilter && (!fieldMapping || !fieldMapping.filterable || ignored));
+ return Boolean(onFilter && (!dataViewField || !dataViewField.filterable || ignoredReason));
}
-export function getFilterInOutPairDisabledWarning(row: TableRow | undefined): string | undefined {
- if (!row || !isFilterInOutPairDisabled(row)) {
+export function getFilterInOutPairDisabledWarning(
+ row: FieldRow | undefined,
+ onFilter: DocViewFilterFn | undefined
+): string | undefined {
+ if (!row || !isFilterInOutPairDisabled(row, onFilter)) {
return undefined;
}
- const {
- field: { fieldMapping },
- value: { ignored },
- } = row;
+ const { dataViewField, ignoredReason } = row;
- if (ignored) {
+ if (ignoredReason) {
return i18n.translate(
'unifiedDocViewer.docViews.table.ignoredValuesCanNotBeSearchedWarningMessage',
{
@@ -57,7 +48,7 @@ export function getFilterInOutPairDisabledWarning(row: TableRow | undefined): st
);
}
- return !fieldMapping
+ return !dataViewField
? i18n.translate(
'unifiedDocViewer.docViews.table.unindexedFieldsCanNotBeSearchedWarningMessage',
{
@@ -67,15 +58,16 @@ export function getFilterInOutPairDisabledWarning(row: TableRow | undefined): st
: undefined;
}
-export const FilterIn: React.FC = ({ Component, row }) => {
+export const FilterIn: React.FC = ({
+ Component,
+ row,
+ onFilter,
+}) => {
if (!row) {
return null;
}
- const {
- action: { onFilter, flattenedField },
- field: { field, fieldMapping },
- } = row;
+ const { dataViewField, name, flattenedValue } = row;
// Filters pair
const filterAddLabel = i18n.translate(
@@ -91,27 +83,28 @@ export const FilterIn: React.FC = ({ Component, row }) => {
return (
onFilter(fieldMapping, flattenedField, '+')}
+ onClick={() => onFilter(dataViewField, flattenedValue, '+')}
>
{filterAddLabel}
);
};
-export const FilterOut: React.FC = ({ Component, row }) => {
+export const FilterOut: React.FC = ({
+ Component,
+ row,
+ onFilter,
+}) => {
if (!row) {
return null;
}
- const {
- action: { onFilter, flattenedField },
- field: { field, fieldMapping },
- } = row;
+ const { dataViewField, name, flattenedValue } = row;
// Filters pair
const filterOutLabel = i18n.translate(
@@ -127,39 +120,42 @@ export const FilterOut: React.FC = ({ Component, row }) => {
return (
onFilter(fieldMapping, flattenedField, '-')}
+ onClick={() => onFilter(dataViewField, flattenedValue, '-')}
>
{filterOutLabel}
);
};
-export function isFilterExistsDisabled(row: TableRow | undefined): boolean {
+export function isFilterExistsDisabled(
+ row: FieldRow | undefined,
+ onFilter: DocViewFilterFn | undefined
+): boolean {
if (!row) {
return false;
}
- const {
- action: { onFilter },
- field: { fieldMapping },
- } = row;
+ const { dataViewField } = row;
- return Boolean(onFilter && (!fieldMapping || !fieldMapping.filterable || fieldMapping.scripted));
+ return Boolean(
+ onFilter && (!dataViewField || !dataViewField.filterable || dataViewField.scripted)
+ );
}
-export function getFilterExistsDisabledWarning(row: TableRow | undefined): string | undefined {
- if (!row || !isFilterExistsDisabled(row)) {
+export function getFilterExistsDisabledWarning(
+ row: FieldRow | undefined,
+ onFilter: DocViewFilterFn | undefined
+): string | undefined {
+ if (!row || !isFilterExistsDisabled(row, onFilter)) {
return undefined;
}
- const {
- field: { fieldMapping },
- } = row;
+ const { dataViewField } = row;
- return fieldMapping?.scripted
+ return dataViewField?.scripted
? i18n.translate(
'unifiedDocViewer.docViews.table.unableToFilterForPresenceOfScriptedFieldsWarningMessage',
{
@@ -169,15 +165,14 @@ export function getFilterExistsDisabledWarning(row: TableRow | undefined): strin
: undefined;
}
-export const FilterExist: React.FC = ({ Component, row }) => {
+export const FilterExist: React.FC<
+ TableActionsProps & { onFilter: DocViewFilterFn | undefined }
+> = ({ Component, row, onFilter }) => {
if (!row) {
return null;
}
- const {
- action: { onFilter },
- field: { field },
- } = row;
+ const { name } = row;
// Filter exists
const filterExistsLabel = i18n.translate(
@@ -191,27 +186,28 @@ export const FilterExist: React.FC = ({ Component, row }) =>
return (
onFilter('_exists_', field, '+')}
+ onClick={() => onFilter('_exists_', name, '+')}
>
{filterExistsLabel}
);
};
-export const ToggleColumn: React.FC = ({ Component, row }) => {
+export const ToggleColumn: React.FC<
+ TableActionsProps & {
+ onToggleColumn: ((field: string) => void) | undefined;
+ }
+> = ({ Component, row, onToggleColumn }) => {
if (!row) {
return null;
}
- const {
- action: { onToggleColumn },
- field: { field },
- } = row;
+ const { name } = row;
if (!onToggleColumn) {
return null;
@@ -227,11 +223,11 @@ export const ToggleColumn: React.FC = ({ Component, row }) =>
return (
onToggleColumn(field)}
+ onClick={() => onToggleColumn(name)}
>
{toggleColumnLabel}
@@ -240,25 +236,31 @@ export const ToggleColumn: React.FC = ({ Component, row }) =>
export function getFieldCellActions({
rows,
- filter,
+ onFilter,
onToggleColumn,
}: {
- rows: TableRow[];
- filter?: DocViewFilterFn;
+ rows: FieldRow[];
+ onFilter?: DocViewFilterFn;
onToggleColumn: ((field: string) => void) | undefined;
}) {
return [
- ...(filter
+ ...(onFilter
? [
({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => {
- return ;
+ return ;
},
]
: []),
...(onToggleColumn
? [
({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => {
- return ;
+ return (
+
+ );
},
]
: []),
@@ -267,18 +269,18 @@ export function getFieldCellActions({
export function getFieldValueCellActions({
rows,
- filter,
+ onFilter,
}: {
- rows: TableRow[];
- filter?: DocViewFilterFn;
+ rows: FieldRow[];
+ onFilter?: DocViewFilterFn;
}) {
- return filter
+ return onFilter
? [
({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => {
- return ;
+ return ;
},
({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => {
- return ;
+ return ;
},
]
: [];
diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_value.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_value.tsx
index dea6010441308..3afe935307ab2 100644
--- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_value.tsx
+++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_value.tsx
@@ -22,7 +22,7 @@ import classnames from 'classnames';
import React, { Fragment, useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { IgnoredReason, TRUNCATE_MAX_HEIGHT } from '@kbn/discover-utils';
-import { FieldRecord } from './table';
+import { FieldRecordLegacy } from '@kbn/unified-doc-viewer/types';
import { getUnifiedDocViewerServices } from '../../plugin';
const DOC_VIEWER_DEFAULT_TRUNCATE_MAX_HEIGHT = 110;
@@ -96,12 +96,13 @@ const IgnoreWarning: React.FC = React.memo(({ rawValue, reas
);
});
-type TableFieldValueProps = Pick & {
- formattedValue: FieldRecord['value']['formattedValue'];
+type TableFieldValueProps = Pick & {
+ formattedValue: FieldRecordLegacy['value']['formattedValue'];
rawValue: unknown;
ignoreReason?: IgnoredReason;
isDetails?: boolean; // true when inside EuiDataGrid cell popover
isLegacy?: boolean; // true when inside legacy table
+ isHighlighted?: boolean; // whether it's matching a search term
};
export const TableFieldValue = ({
@@ -111,6 +112,7 @@ export const TableFieldValue = ({
ignoreReason,
isDetails,
isLegacy,
+ isHighlighted,
}: TableFieldValueProps) => {
const { euiTheme } = useEuiTheme();
const { uiSettings } = getUnifiedDocViewerServices();
@@ -158,6 +160,7 @@ export const TableFieldValue = ({
const valueClasses = classnames('kbnDocViewer__value', {
'kbnDocViewer__value--truncated': shouldTruncate,
+ 'kbnDocViewer__value--highlighted': isHighlighted && !isDetails,
});
return (
diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_filters.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_filters.tsx
index 5c3e7862ea6c8..50a3064b0df4d 100644
--- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_filters.tsx
+++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_filters.tsx
@@ -19,14 +19,21 @@ import {
type FieldTypeFilterProps,
} from '@kbn/unified-field-list/src/components/field_list_filters/field_type_filter';
import { getUnifiedDocViewerServices } from '../../plugin';
+import { FieldRow } from './field_row';
export const LOCAL_STORAGE_KEY_SEARCH_TERM = 'discover:searchText';
export const LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES = 'unifiedDocViewer:selectedFieldTypes';
const searchPlaceholder = i18n.translate('unifiedDocViewer.docView.table.searchPlaceHolder', {
- defaultMessage: 'Search field names',
+ defaultMessage: 'Search field names or values',
});
+export enum TermMatch {
+ name = 'name',
+ value = 'value',
+ both = 'both',
+}
+
interface TableFiltersCommonProps {
// search
searchTerm: string;
@@ -108,12 +115,9 @@ const getStoredFieldTypes = (storage: Storage) => {
return Array.isArray(parsedFieldTypes) ? parsedFieldTypes : [];
};
-interface UseTableFiltersReturn extends TableFiltersCommonProps {
- onFilterField: (
- fieldName: string,
- fieldDisplayName: string | undefined,
- fieldType: string | undefined
- ) => boolean;
+export interface UseTableFiltersReturn extends TableFiltersCommonProps {
+ onFilterField: (row: FieldRow) => boolean;
+ onFindSearchTermMatch: (row: FieldRow, term: string) => TermMatch | null;
}
export const useTableFilters = (storage: Storage): UseTableFiltersReturn => {
@@ -138,13 +142,34 @@ export const useTableFilters = (storage: Storage): UseTableFiltersReturn => {
[storage, setSelectedFieldTypes]
);
- const onFilterField: UseTableFiltersReturn['onFilterField'] = useCallback(
- (fieldName, fieldDisplayName, fieldType) => {
- const term = searchTerm?.trim();
+ const onFindSearchTermMatch: UseTableFiltersReturn['onFindSearchTermMatch'] = useCallback(
+ (row, term) => {
+ const { name, dataViewField } = row;
+
+ let termMatch: TermMatch | null = null;
+
+ if (fieldNameWildcardMatcher({ name, displayName: dataViewField?.customLabel }, term)) {
+ termMatch = TermMatch.name;
+ }
+
if (
- term &&
- !fieldNameWildcardMatcher({ name: fieldName, displayName: fieldDisplayName }, term)
+ (row.formattedAsText || '').toLowerCase().includes(term.toLowerCase()) ||
+ (JSON.stringify(row.flattenedValue) || '').toLowerCase().includes(term.toLowerCase())
) {
+ termMatch = termMatch ? TermMatch.both : TermMatch.value;
+ }
+
+ return termMatch;
+ },
+ []
+ );
+
+ const onFilterField: UseTableFiltersReturn['onFilterField'] = useCallback(
+ (row) => {
+ const { fieldType } = row;
+ const term = searchTerm?.trim();
+
+ if (term && !onFindSearchTermMatch(row, term)) {
return false;
}
@@ -154,7 +179,7 @@ export const useTableFilters = (storage: Storage): UseTableFiltersReturn => {
return true;
},
- [searchTerm, selectedFieldTypes]
+ [searchTerm, selectedFieldTypes, onFindSearchTermMatch]
);
return useMemo(
@@ -166,7 +191,15 @@ export const useTableFilters = (storage: Storage): UseTableFiltersReturn => {
onChangeFieldTypes,
// the actual filtering function
onFilterField,
+ onFindSearchTermMatch,
}),
- [searchTerm, onChangeSearchTerm, selectedFieldTypes, onChangeFieldTypes, onFilterField]
+ [
+ searchTerm,
+ onChangeSearchTerm,
+ selectedFieldTypes,
+ onChangeFieldTypes,
+ onFilterField,
+ onFindSearchTermMatch,
+ ]
);
};
diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/test_filters.test.ts b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/test_filters.test.ts
index 546c7fb3a0d09..6d0a8c5dfc96c 100644
--- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/test_filters.test.ts
+++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/test_filters.test.ts
@@ -14,9 +14,54 @@ import {
LOCAL_STORAGE_KEY_SEARCH_TERM,
LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES,
} from './table_filters';
+import { FieldRow } from './field_row';
+import { buildDataTableRecord } from '@kbn/discover-utils';
+import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub';
+import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
const storage = new Storage(window.localStorage);
+const hit = buildDataTableRecord(
+ {
+ _ignored: [],
+ _index: 'test',
+ _id: '1',
+ _source: {
+ 'extension.keyword': 'zip',
+ bytes: 500,
+ '@timestamp': '2021-01-01T00:00:00',
+ },
+ },
+ dataView
+);
+const rowExtensionKeyword = new FieldRow({
+ name: 'extension.keyword',
+ flattenedValue: 'zip',
+ hit,
+ dataView,
+ fieldFormats: {} as FieldFormatsStart,
+ isPinned: false,
+ columnsMeta: undefined,
+});
+const rowBytes = new FieldRow({
+ name: 'bytes',
+ flattenedValue: 500,
+ hit,
+ dataView,
+ fieldFormats: {} as FieldFormatsStart,
+ isPinned: false,
+ columnsMeta: undefined,
+});
+const rowTimestamp = new FieldRow({
+ name: '@timestamp',
+ flattenedValue: '2021-01-01T00:00:00',
+ hit,
+ dataView,
+ fieldFormats: {} as FieldFormatsStart,
+ isPinned: false,
+ columnsMeta: undefined,
+});
+
describe('useTableFilters', () => {
beforeAll(() => {
jest.useFakeTimers();
@@ -34,8 +79,8 @@ describe('useTableFilters', () => {
expect(result.current.searchTerm).toBe('');
expect(result.current.selectedFieldTypes).toEqual([]);
- expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(true);
- expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(true);
+ expect(result.current.onFilterField(rowExtensionKeyword)).toBe(true);
+ expect(result.current.onFilterField(rowBytes)).toBe(true);
expect(storage.get(LOCAL_STORAGE_KEY_SEARCH_TERM)).toBeNull();
});
@@ -47,8 +92,8 @@ describe('useTableFilters', () => {
result.current.onChangeSearchTerm('ext');
});
- expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(true);
- expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(false);
+ expect(result.current.onFilterField(rowExtensionKeyword)).toBe(true);
+ expect(result.current.onFilterField(rowBytes)).toBe(false);
expect(storage.get(LOCAL_STORAGE_KEY_SEARCH_TERM)).toBe('ext');
});
@@ -60,22 +105,22 @@ describe('useTableFilters', () => {
result.current.onChangeFieldTypes(['number']);
});
- expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(false);
- expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(true);
+ expect(result.current.onFilterField(rowExtensionKeyword)).toBe(false);
+ expect(result.current.onFilterField(rowBytes)).toBe(true);
act(() => {
result.current.onChangeFieldTypes(['keyword']);
});
- expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(true);
- expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(false);
+ expect(result.current.onFilterField(rowExtensionKeyword)).toBe(true);
+ expect(result.current.onFilterField(rowBytes)).toBe(false);
act(() => {
result.current.onChangeFieldTypes(['number', 'keyword']);
});
- expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(true);
- expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(true);
+ expect(result.current.onFilterField(rowExtensionKeyword)).toBe(true);
+ expect(result.current.onFilterField(rowBytes)).toBe(true);
jest.advanceTimersByTime(600);
expect(storage.get(LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES)).toBe('["number","keyword"]');
@@ -89,30 +134,69 @@ describe('useTableFilters', () => {
result.current.onChangeFieldTypes(['keyword']);
});
- expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(true);
- expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(false);
+ expect(result.current.onFilterField(rowExtensionKeyword)).toBe(true);
+ expect(result.current.onFilterField(rowBytes)).toBe(false);
act(() => {
result.current.onChangeSearchTerm('ext');
result.current.onChangeFieldTypes(['number']);
});
- expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(false);
- expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(false);
+ expect(result.current.onFilterField(rowExtensionKeyword)).toBe(false);
+ expect(result.current.onFilterField(rowBytes)).toBe(false);
act(() => {
result.current.onChangeSearchTerm('bytes');
result.current.onChangeFieldTypes(['number']);
});
- expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(false);
- expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(true);
+ expect(result.current.onFilterField(rowExtensionKeyword)).toBe(false);
+ expect(result.current.onFilterField(rowBytes)).toBe(true);
jest.advanceTimersByTime(600);
expect(storage.get(LOCAL_STORAGE_KEY_SEARCH_TERM)).toBe('bytes');
expect(storage.get(LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES)).toBe('["number"]');
});
+ it('should filter by field value and field type', () => {
+ const { result } = renderHook(() => useTableFilters(storage));
+
+ expect(result.current.onFilterField(rowTimestamp)).toBe(true);
+ expect(result.current.onFilterField(rowExtensionKeyword)).toBe(true);
+ expect(result.current.onFilterField(rowBytes)).toBe(true);
+
+ act(() => {
+ result.current.onChangeSearchTerm('500');
+ result.current.onChangeFieldTypes(['number']);
+ });
+
+ expect(result.current.onFilterField(rowTimestamp)).toBe(false);
+ expect(result.current.onFilterField(rowExtensionKeyword)).toBe(false);
+ expect(result.current.onFilterField(rowBytes)).toBe(true);
+
+ act(() => {
+ result.current.onChangeSearchTerm('2021');
+ result.current.onChangeFieldTypes(['number']);
+ });
+
+ expect(result.current.onFilterField(rowTimestamp)).toBe(false);
+ expect(result.current.onFilterField(rowExtensionKeyword)).toBe(false);
+ expect(result.current.onFilterField(rowBytes)).toBe(false);
+
+ act(() => {
+ result.current.onChangeSearchTerm('2021');
+ result.current.onChangeFieldTypes(['date']);
+ });
+
+ expect(result.current.onFilterField(rowTimestamp)).toBe(true);
+ expect(result.current.onFilterField(rowExtensionKeyword)).toBe(false);
+ expect(result.current.onFilterField(rowBytes)).toBe(false);
+
+ jest.advanceTimersByTime(600);
+ expect(storage.get(LOCAL_STORAGE_KEY_SEARCH_TERM)).toBe('2021');
+ expect(storage.get(LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES)).toBe('["date"]');
+ });
+
it('should restore previous filters', () => {
storage.set(LOCAL_STORAGE_KEY_SEARCH_TERM, 'bytes');
storage.set(LOCAL_STORAGE_KEY_SELECTED_FIELD_TYPES, '["number"]');
@@ -122,8 +206,8 @@ describe('useTableFilters', () => {
expect(result.current.searchTerm).toBe('bytes');
expect(result.current.selectedFieldTypes).toEqual(['number']);
- expect(result.current.onFilterField('extension', undefined, 'keyword')).toBe(false);
- expect(result.current.onFilterField('bytes', undefined, 'number')).toBe(true);
- expect(result.current.onFilterField('bytes_counter', undefined, 'counter')).toBe(false);
+ expect(result.current.onFilterField(rowExtensionKeyword)).toBe(false);
+ expect(result.current.onFilterField(rowBytes)).toBe(true);
+ expect(result.current.onFilterField(rowTimestamp)).toBe(false);
});
});
diff --git a/test/functional/apps/context/_filters.ts b/test/functional/apps/context/_filters.ts
index bc404b45f22c0..4a3f8b3a9998f 100644
--- a/test/functional/apps/context/_filters.ts
+++ b/test/functional/apps/context/_filters.ts
@@ -42,7 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('inclusive filter should be addable via expanded data grid rows', async function () {
await retry.waitFor(`filter ${TEST_ANCHOR_FILTER_FIELD} in filterbar`, async () => {
await dataGrid.clickRowToggle({ isAnchorRow: true, renderMoreRows: true });
- await PageObjects.discover.findFieldByNameInDocViewer(TEST_ANCHOR_FILTER_FIELD);
+ await PageObjects.discover.findFieldByNameOrValueInDocViewer(TEST_ANCHOR_FILTER_FIELD);
await dataGrid.clickFieldActionInFlyout(
TEST_ANCHOR_FILTER_FIELD,
'addFilterForValueButton'
diff --git a/test/functional/apps/discover/group3/_doc_viewer.ts b/test/functional/apps/discover/group3/_doc_viewer.ts
index 3d3562e10beb4..bc71c82289071 100644
--- a/test/functional/apps/discover/group3/_doc_viewer.ts
+++ b/test/functional/apps/discover/group3/_doc_viewer.ts
@@ -66,13 +66,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should be able to search by string', async function () {
- await discover.findFieldByNameInDocViewer('geo');
+ await discover.findFieldByNameOrValueInDocViewer('geo');
await retry.waitFor('first updates', async () => {
return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 4;
});
- await discover.findFieldByNameInDocViewer('.sr');
+ await discover.findFieldByNameOrValueInDocViewer('.sr');
await retry.waitFor('second updates', async () => {
return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 2;
@@ -80,21 +80,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should be able to search by wildcard', async function () {
- await discover.findFieldByNameInDocViewer('relatedContent*image');
+ await discover.findFieldByNameOrValueInDocViewer('relatedContent*image');
await retry.waitFor('updates', async () => {
return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 2;
});
});
it('should be able to search with spaces as wildcard', async function () {
- await discover.findFieldByNameInDocViewer('relatedContent image');
+ await discover.findFieldByNameOrValueInDocViewer('relatedContent image');
await retry.waitFor('updates', async () => {
return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 4;
});
});
it('should be able to search with fuzzy search (1 typo)', async function () {
- await discover.findFieldByNameInDocViewer('rel4tedContent.art');
+ await discover.findFieldByNameOrValueInDocViewer('rel4tedContent.art');
await retry.waitFor('updates', async () => {
return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 3;
@@ -102,7 +102,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should ignore empty search', async function () {
- await discover.findFieldByNameInDocViewer(' '); // only spaces
+ await discover.findFieldByNameOrValueInDocViewer(' '); // only spaces
await retry.waitFor('the clear button', async () => {
return await testSubjects.exists('clearSearchButton');
@@ -113,6 +113,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length > 0;
});
});
+
+ it('should be able to search by field value', async function () {
+ await discover.findFieldByNameOrValueInDocViewer('time');
+
+ await retry.waitFor('updates', async () => {
+ return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 5;
+ });
+ });
+
+ it('should be able to search by field raw value', async function () {
+ await discover.findFieldByNameOrValueInDocViewer('2015-09-22T23:50:13.253Z');
+
+ await retry.waitFor('updates', async () => {
+ return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length === 3;
+ });
+ });
});
describe('filter by field type', function () {
@@ -220,7 +236,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should hide fields with null values ', async function () {
- await discover.findFieldByNameInDocViewer('machine');
+ await discover.findFieldByNameOrValueInDocViewer('machine');
const results = (await find.allByCssSelector('.kbnDocViewer__fieldName')).length;
const hideNullValuesSwitch = await testSubjects.find(
'unifiedDocViewerHideNullValuesSwitch'
diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index 54dae77ff3839..1474e9d315538 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -444,7 +444,7 @@ export class DiscoverPageObject extends FtrService {
return await this.find.byClassName('monaco-editor');
}
- public async findFieldByNameInDocViewer(name: string) {
+ public async findFieldByNameOrValueInDocViewer(name: string) {
const fieldSearch = await this.testSubjects.find('unifiedDocViewerFieldsSearchInput');
await fieldSearch.type(name);
}
diff --git a/x-pack/packages/kbn-elastic-assistant-common/env/http-client.env.json b/x-pack/packages/kbn-elastic-assistant-common/env/http-client.env.json
index 3f48e01e099fa..2c93670ea1f85 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/env/http-client.env.json
+++ b/x-pack/packages/kbn-elastic-assistant-common/env/http-client.env.json
@@ -4,11 +4,22 @@
"port": "5601",
"basePath": "",
"elasticApiVersion": "1",
+ "elasticsearch": {
+ "host": "localhost",
+ "port": "9200"
+ },
"auth": {
- "username": "elastic",
- "password": "changeme"
+ "admin": {
+ "username": "elastic",
+ "password": "changeme"
+ },
+ "assistant_all": {
+ "username": "assistant_all",
+ "password": "changeme"
+ }
},
"appContext": {
+ "management": "%7B%22type%22%3A%22application%22%2C%22name%22%3A%22management%22%2C%22url%22%3A%22%2Fkbn%2Fapp%2Fmanagement%22%2C%22page%22%3A%22%22%7D",
"security": "%7B%22type%22%3A%22application%22%2C%22name%22%3A%22securitySolutionUI%22%2C%22url%22%3A%22%2Fkbn%2Fapp%2Fsecurity%22%7D"
}
}
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts
index f18bfb57fa359..ca00075b30e36 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts
@@ -27,6 +27,15 @@ export const KnowledgeBaseResponse = z.object({
success: z.boolean().optional(),
});
+export type CreateKnowledgeBaseRequestQuery = z.infer;
+export const CreateKnowledgeBaseRequestQuery = z.object({
+ /**
+ * Optional ELSER modelId to use when setting up the Knowledge Base
+ */
+ modelId: z.string().optional(),
+});
+export type CreateKnowledgeBaseRequestQueryInput = z.input;
+
export type CreateKnowledgeBaseRequestParams = z.infer;
export const CreateKnowledgeBaseRequestParams = z.object({
/**
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml
index 4342d334aad1a..8b4e6bfaca5ec 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml
@@ -18,6 +18,12 @@ paths:
description: The KnowledgeBase `resource` value.
schema:
type: string
+ - name: modelId
+ in: query
+ description: Optional ELSER modelId to use when setting up the Knowledge Base
+ required: false
+ schema:
+ type: string
responses:
200:
description: Indicates a successful call.
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/bulk_crud_knowledge_base_entries_route.http b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/bulk_crud_knowledge_base_entries_route.http
index d8ad084a792a1..873a7299820f0 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/bulk_crud_knowledge_base_entries_route.http
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/bulk_crud_knowledge_base_entries_route.http
@@ -3,7 +3,7 @@ POST http://{{host}}:{{port}}{{basePath}}/internal/elastic_assistant/knowledge_b
kbn-xsrf: "true"
Content-Type: application/json
Elastic-Api-Version: {{elasticApiVersion}}
-Authorization: Basic {{auth.username}} {{auth.password}}
+Authorization: Basic {{auth.admin.username}} {{auth.admin.password}}
X-Kbn-Context: {{appContext.security}}
{
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.http b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.http
index 88ad6555da50f..6bd8dc7ba288b 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.http
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.http
@@ -1,45 +1,151 @@
-### Create Document Entry
+### Create Document Entry [Admin] [Private]
POST http://{{host}}:{{port}}{{basePath}}/internal/elastic_assistant/knowledge_base/entries
kbn-xsrf: "true"
Content-Type: application/json
Elastic-Api-Version: {{elasticApiVersion}}
-Authorization: Basic {{auth.username}} {{auth.password}}
+Authorization: Basic {{auth.admin.username}} {{auth.admin.password}}
X-Kbn-Context: {{appContext.security}}
{
"type": "document",
- "name": "Favorites",
+ "name": "Document Entry [Admin] [Private]",
"kbResource": "user",
"source": "api",
"required": true,
"text": "My favorite food is Dan Bing"
}
-### Create Index Entry
+### Create Document Entry [Admin] [Global]
POST http://{{host}}:{{port}}{{basePath}}/internal/elastic_assistant/knowledge_base/entries
kbn-xsrf: "true"
Content-Type: application/json
Elastic-Api-Version: {{elasticApiVersion}}
-Authorization: Basic {{auth.username}} {{auth.password}}
+Authorization: Basic {{auth.admin.username}} {{auth.admin.password}}
+X-Kbn-Context: {{appContext.security}}
+
+{
+ "type": "document",
+ "name": "Document Entry [Admin] [Global]",
+ "kbResource": "user",
+ "source": "api",
+ "required": true,
+ "text": "My favorite food is pizza",
+ "users": []
+}
+
+### Create Document Entry [Assistant All] [Private]
+POST http://{{host}}:{{port}}{{basePath}}/internal/elastic_assistant/knowledge_base/entries
+kbn-xsrf: "true"
+Content-Type: application/json
+Elastic-Api-Version: {{elasticApiVersion}}
+Authorization: Basic {{auth.assistant_all.username}} {{auth.assistant_all.password}}
+X-Kbn-Context: {{appContext.security}}
+
+{
+ "type": "document",
+ "name": "Document Entry [Assistant All] [Private]",
+ "kbResource": "user",
+ "source": "api",
+ "required": true,
+ "text": "My favorite food is popcorn"
+}
+
+### Create Document Entry [Assistant All] [Global]
+POST http://{{host}}:{{port}}{{basePath}}/internal/elastic_assistant/knowledge_base/entries
+kbn-xsrf: "true"
+Content-Type: application/json
+Elastic-Api-Version: {{elasticApiVersion}}
+Authorization: Basic {{auth.assistant_all.username}} {{auth.assistant_all.password}}
+X-Kbn-Context: {{appContext.security}}
+
+{
+ "type": "document",
+ "name": "Document Entry [Assistant All] [Global]",
+ "kbResource": "user",
+ "source": "api",
+ "required": true,
+ "text": "My favorite food is peaches",
+ "users": []
+}
+
+### Create Index Entry [Admin] [Private]
+POST http://{{host}}:{{port}}{{basePath}}/internal/elastic_assistant/knowledge_base/entries
+kbn-xsrf: "true"
+Content-Type: application/json
+Elastic-Api-Version: {{elasticApiVersion}}
+Authorization: Basic {{auth.admin.username}} {{auth.admin.password}}
X-Kbn-Context: {{appContext.security}}
{
"type": "index",
- "name": "SpongBotSlackConnector",
+ "name": "Slackbot-test Index Entry [Admin] [Private]",
"namespace": "default",
- "index": "spongbot-slack",
+ "index": "slackbot-test",
+ "field": "semantic_text",
+ "description": "Use this index to search for the user's Slack messages.",
+ "queryDescription":
+ "The free text search that the user wants to perform over this dataset. So if asking \"what are my slack messages from last week about failed tests\", the query would be \"A test has failed! failing test failed test\"",
+ "outputFields": ["author", "text", "timestamp"]
+}
+
+### Create Index Entry [Admin] [Global]
+POST http://{{host}}:{{port}}{{basePath}}/internal/elastic_assistant/knowledge_base/entries
+kbn-xsrf: "true"
+Content-Type: application/json
+Elastic-Api-Version: {{elasticApiVersion}}
+Authorization: Basic {{auth.admin.username}} {{auth.admin.password}}
+X-Kbn-Context: {{appContext.security}}
+
+{
+ "type": "index",
+ "name": "Slackbot-test Index Entry [Admin] [Global]",
+ "namespace": "default",
+ "index": "slackbot-test",
"field": "semantic_text",
"description": "Use this index to search for the user's Slack messages.",
"queryDescription":
"The free text search that the user wants to perform over this dataset. So if asking \"what are my slack messages from last week about failed tests\", the query would be \"A test has failed! failing test failed test\"",
- "inputSchema": [
- {
- "fieldName": "author",
- "fieldType": "string",
- "description": "The author of the message. So if asking for recent messages from Stan, you would provide 'Stan' as the author."
- }
- ],
- "outputFields": ["author", "text", "timestamp"]
+ "outputFields": ["author", "text", "timestamp"],
+ "users": []
+}
+
+### Create Index Entry [Assistant All] [Private]
+POST http://{{host}}:{{port}}{{basePath}}/internal/elastic_assistant/knowledge_base/entries
+kbn-xsrf: "true"
+Content-Type: application/json
+Elastic-Api-Version: {{elasticApiVersion}}
+Authorization: Basic {{auth.assistant_all.username}} {{auth.assistant_all.password}}
+X-Kbn-Context: {{appContext.security}}
+
+{
+ "type": "index",
+ "name": "Slackbot-test Index Entry [Assistant All] [Private]",
+ "namespace": "default",
+ "index": "slackbot-test",
+ "field": "semantic_text",
+ "description": "Use this index to search for the user's Slack messages.",
+ "queryDescription": "The free text search that the user wants to perform over this dataset. So if asking \"what are my slack messages from last week about failed tests\", the query would be \"A test has failed! failing test failed test\"",
+ "outputFields": ["author", "text", "timestamp" ]
+}
+
+### Create Index Entry [Assistant All] [Global]
+POST http://{{host}}:{{port}}{{basePath}}/internal/elastic_assistant/knowledge_base/entries
+kbn-xsrf: "true"
+Content-Type: application/json
+Elastic-Api-Version: {{elasticApiVersion}}
+Authorization: Basic {{auth.assistant_all.username}} {{auth.assistant_all.password}}
+X-Kbn-Context: {{appContext.security}}
+
+{
+ "type": "index",
+ "name": "Slackbot-test Index Entry [Assistant All] [Global]",
+ "namespace": "default",
+ "index": "slackbot-test",
+ "field": "semantic_text",
+ "description": "Use this index to search for the user's Slack messages.",
+ "queryDescription": "The free text search that the user wants to perform over this dataset. So if asking \"what are my slack messages from last week about failed tests\", the query would be \"A test has failed! failing test failed test\"",
+ "outputFields": ["author", "text", "timestamp" ],
+ "users": []
}
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/find_knowledge_base_entries_route.http b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/find_knowledge_base_entries_route.http
index 3d90053767dfe..81733c62838a4 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/find_knowledge_base_entries_route.http
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/find_knowledge_base_entries_route.http
@@ -1,6 +1,6 @@
### Find all knowledge base entries
GET http://{{host}}:{{port}}{{basePath}}/internal/elastic_assistant/knowledge_base/entries/_find
Elastic-Api-Version: {{elasticApiVersion}}
-Authorization: Basic {{auth.username}} {{auth.password}}
+Authorization: Basic {{auth.admin.username}} {{auth.admin.password}}
X-Kbn-Context: {{appContext.security}}
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/mocks.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/mocks.ts
index a4828c2f24d91..24a43bd3182df 100644
--- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/mocks.ts
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/mocks.ts
@@ -16,12 +16,4 @@ export const indexEntryMock: IndexEntryCreateFields = {
description: "Use this index to search for the user's Slack messages.",
queryDescription:
'The free text search that the user wants to perform over this dataset. So if asking "what are my slack messages from last week about failed tests", the query would be "A test has failed! failing test failed test".',
- inputSchema: [
- {
- fieldName: 'author',
- fieldType: 'string',
- description:
- "The author of the message. So if asking for recent messages from Stan, you would provide 'Stan' as the author.",
- },
- ],
};
diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/utils/spaces_roles_users_data.http b/x-pack/packages/kbn-elastic-assistant-common/impl/utils/spaces_roles_users_data.http
new file mode 100644
index 0000000000000..16cdf270565a4
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant-common/impl/utils/spaces_roles_users_data.http
@@ -0,0 +1,182 @@
+### Create Space-X
+POST http://{{host}}:{{port}}{{basePath}}/api/spaces/space
+kbn-xsrf: "true"
+Content-Type: application/json
+Elastic-Api-Version: {{elasticApiVersion}}
+Authorization: Basic {{auth.admin.username}} {{auth.admin.password}}
+
+{
+ "name": "Space-X",
+ "id": "space-x",
+ "initials": "🚀",
+ "color": "#9170B8",
+ "disabledFeatures": [],
+ "imageUrl": ""
+}
+
+### Create Space-Y
+POST http://{{host}}:{{port}}{{basePath}}/api/spaces/space
+kbn-xsrf: "true"
+Content-Type: application/json
+Elastic-Api-Version: {{elasticApiVersion}}
+Authorization: Basic {{auth.admin.username}} {{auth.admin.password}}
+
+{
+ "name": "Space-Y",
+ "id": "space-y",
+ "initials": "🛰",
+ "color": "#DA8B45",
+ "disabledFeatures": [],
+ "imageUrl": ""
+}
+
+### Create Assistant All Role - All Spaces, All Features
+PUT http://{{host}}:{{port}}{{basePath}}/api/security/role/assistant_all
+kbn-xsrf: "true"
+Content-Type: application/json
+Elastic-Api-Version: {{elasticApiVersion}}
+Authorization: Basic {{auth.admin.username}} {{auth.admin.password}}
+
+{
+ "description": "Grants access to all Security Assistant features in all spaces",
+ "elasticsearch": {
+ "cluster": [
+ "all"
+ ],
+ "indices": [
+ {
+ "names": [
+ "*"
+ ],
+ "privileges": [
+ "all"
+ ],
+ "field_security": {
+ "grant": [
+ "*"
+ ],
+ "except": []
+ }
+ }
+ ],
+ "run_as": []
+ },
+ "kibana": [
+ {
+ "spaces": [
+ "*"
+ ],
+ "base": [],
+ "feature": {
+ "siem": [
+ "all"
+ ],
+ "securitySolutionCases": [
+ "all"
+ ],
+ "securitySolutionAssistant": [
+ "all"
+ ],
+ "securitySolutionAttackDiscovery": [
+ "all"
+ ],
+ "aiAssistantManagementSelection": [
+ "all"
+ ],
+ "searchInferenceEndpoints": [
+ "all"
+ ],
+ "dev_tools": [
+ "all"
+ ],
+ "actions": [
+ "all"
+ ],
+ "indexPatterns": [
+ "all"
+ ]
+ }
+ }
+ ]
+}
+
+### Create Assistant All User - All Spaces, All Features
+POST http://{{host}}:{{port}}{{basePath}}/internal/security/users/assistant_all
+kbn-xsrf: "true"
+Content-Type: application/json
+Elastic-Api-Version: {{elasticApiVersion}}
+Authorization: Basic {{auth.admin.username}} {{auth.admin.password}}
+
+{
+ "password": "{{auth.assistant_all.password}}",
+ "username": "{{auth.assistant_all.username}}",
+ "full_name": "Assistant All",
+ "email": "",
+ "roles": [
+ "assistant_all"
+ ]
+}
+
+### Create Inference Endpoint
+PUT http://{{elasticsearch.host}}:{{elasticsearch.port}}/_inference/sparse_embedding/elser_model_2
+Content-Type: application/json
+Authorization: Basic {{auth.admin.username}} {{auth.admin.password}}
+
+{
+ "service": "elser",
+ "service_settings": {
+ "num_allocations": 1,
+ "num_threads": 1
+ }
+}
+
+### Create Slackbot Mappings
+PUT http://{{elasticsearch.host}}:{{elasticsearch.port}}/slackbot-test
+Content-Type: application/json
+Authorization: Basic {{auth.admin.username}} {{auth.admin.password}}
+
+{
+ "settings": {
+ "number_of_shards": 1
+ },
+ "mappings": {
+ "dynamic": "true",
+ "properties": {
+ "semantic_text": {
+ "type": "semantic_text",
+ "inference_id": "elser_model_2",
+ "model_settings": {
+ "task_type": "sparse_embedding"
+ }
+ },
+ "text": {
+ "type": "text",
+ "copy_to": [
+ "semantic_text"
+ ]
+ }
+ }
+ }
+}
+
+### Create Slackbot Document
+POST http://{{elasticsearch.host}}:{{elasticsearch.port}}/slackbot-test/_doc
+Content-Type: application/json
+Authorization: Basic {{auth.admin.username}} {{auth.admin.password}}
+
+{
+ "subtype": null,
+ "author": "spong",
+ "edited_ts": null,
+ "thread_ts": "1727113718.664029",
+ "channel": "dev-details",
+ "text": "The Dude: That rug really tied the room together.",
+ "id": "C0A6H3AA1BL-1727115800.120029",
+ "type": "message",
+ "reply_count": null,
+ "ts": "1727115800.120029",
+ "latest_reply": null
+}
+
+
+
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.tsx
index be6fea6649bbb..e73bfa15e66be 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings.tsx
@@ -5,19 +5,12 @@
* 2.0.
*/
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiFormRow,
- EuiRange,
- EuiSpacer,
- EuiText,
- useGeneratedHtmlId,
-} from '@elastic/eui';
+import { EuiFlexGroup, EuiFormRow, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
import { css } from '@emotion/react';
import React from 'react';
import { KnowledgeBaseConfig } from '../../assistant/types';
+import { AlertsRange } from '../../knowledge_base/alerts_range';
import * as i18n from '../../knowledge_base/translations';
export const MIN_LATEST_ALERTS = 10;
@@ -32,8 +25,6 @@ interface Props {
}
const AlertsSettingsComponent = ({ knowledgeBase, setUpdatedKnowledgeBaseSettings }: Props) => {
- const inputRangeSliderId = useGeneratedHtmlId({ prefix: 'inputRangeSlider' });
-
return (
<>
-
- setUpdatedKnowledgeBaseSettings({
- ...knowledgeBase,
- latestAlerts: Number(e.currentTarget.value),
- })
- }
- showTicks
- step={TICK_INTERVAL}
- value={knowledgeBase.latestAlerts}
+
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings_management.tsx b/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings_management.tsx
new file mode 100644
index 0000000000000..d103c1a8c03c2
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/alerts/settings/alerts_settings_management.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
+import React from 'react';
+import { KnowledgeBaseConfig } from '../../assistant/types';
+import { AlertsRange } from '../../knowledge_base/alerts_range';
+import * as i18n from '../../knowledge_base/translations';
+
+interface Props {
+ knowledgeBase: KnowledgeBaseConfig;
+ setUpdatedKnowledgeBaseSettings: React.Dispatch>;
+}
+
+export const AlertsSettingsManagement: React.FC = React.memo(
+ ({ knowledgeBase, setUpdatedKnowledgeBaseSettings }) => {
+ return (
+
+
+ {i18n.ALERTS_LABEL}
+
+
+
+
+ {i18n.LATEST_AND_RISKIEST_OPEN_ALERTS(knowledgeBase.latestAlerts)}
+ {i18n.YOUR_ANONYMIZATION_SETTINGS}
+
+
+
+
+
+ );
+ }
+);
+
+AlertsSettingsManagement.displayName = 'AlertsSettingsManagement';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_create_knowledge_base_entry.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_create_knowledge_base_entry.tsx
index eaf9a32fde81a..f4edcb0d8d442 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_create_knowledge_base_entry.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_create_knowledge_base_entry.tsx
@@ -78,6 +78,13 @@ export const useCreateKnowledgeBaseEntry = ({
onSettled: () => {
invalidateKnowledgeBaseEntries();
},
+ onSuccess: () => {
+ toasts?.addSuccess({
+ title: i18n.translate('xpack.elasticAssistant.knowledgeBase.entries.createSuccessTitle', {
+ defaultMessage: 'Knowledge Base Entry created',
+ }),
+ });
+ },
}
);
};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_knowledge_base_entries.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_knowledge_base_entries.ts
index 43d70af7dd255..b41119779b21d 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_knowledge_base_entries.ts
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_knowledge_base_entries.ts
@@ -5,8 +5,9 @@
* 2.0.
*/
-import { HttpSetup } from '@kbn/core/public';
+import { HttpSetup, type IHttpFetchError, type ResponseErrorBody } from '@kbn/core/public';
import { useQuery, useQueryClient } from '@tanstack/react-query';
+import type { IToasts } from '@kbn/core-notifications-browser';
import {
API_VERSIONS,
ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_FIND,
@@ -15,11 +16,14 @@ import {
} from '@kbn/elastic-assistant-common';
import { useCallback } from 'react';
+import { i18n } from '@kbn/i18n';
export interface UseKnowledgeBaseEntriesParams {
http: HttpSetup;
- query: FindKnowledgeBaseEntriesRequestQuery;
+ query?: FindKnowledgeBaseEntriesRequestQuery;
signal?: AbortSignal | undefined;
+ toasts?: IToasts;
+ enabled?: boolean; // For disabling if FF is off
}
const defaultQuery: FindKnowledgeBaseEntriesRequestQuery = {
@@ -50,6 +54,8 @@ export const useKnowledgeBaseEntries = ({
http,
query = defaultQuery,
signal,
+ toasts,
+ enabled = false,
}: UseKnowledgeBaseEntriesParams) =>
useQuery(
KNOWLEDGE_BASE_ENTRY_QUERY_KEY,
@@ -64,8 +70,18 @@ export const useKnowledgeBaseEntries = ({
}
),
{
+ enabled,
keepPreviousData: true,
initialData: { page: 1, perPage: 100, total: 0, data: [] },
+ onError: (error: IHttpFetchError) => {
+ if (error.name !== 'AbortError') {
+ toasts?.addError(error, {
+ title: i18n.translate('xpack.elasticAssistant.knowledgeBase.fetchError', {
+ defaultMessage: 'Error fetching Knowledge Base entries',
+ }),
+ });
+ }
+ },
}
);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_update_knowledge_base_entries.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_update_knowledge_base_entries.tsx
new file mode 100644
index 0000000000000..e717c6786cd25
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/entries/use_update_knowledge_base_entries.tsx
@@ -0,0 +1,94 @@
+/*
+ * 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 { useMutation } from '@tanstack/react-query';
+import type { HttpSetup, IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser';
+import type { IToasts } from '@kbn/core-notifications-browser';
+import { i18n } from '@kbn/i18n';
+
+import {
+ API_VERSIONS,
+ ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BULK_ACTION,
+ KnowledgeBaseEntryBulkCrudActionResponse,
+ PerformKnowledgeBaseEntryBulkActionRequestBody,
+} from '@kbn/elastic-assistant-common';
+import { useInvalidateKnowledgeBaseEntries } from './use_knowledge_base_entries';
+
+const BULK_UPDATE_KNOWLEDGE_BASE_ENTRY_MUTATION_KEY = [
+ ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BULK_ACTION,
+ API_VERSIONS.internal.v1,
+ 'UPDATE',
+];
+
+export interface UseUpdateKnowledgeBaseEntriesParams {
+ http: HttpSetup;
+ signal?: AbortSignal;
+ toasts?: IToasts;
+}
+
+/**
+ * Hook for updating Knowledge Base Entries by id or query.
+ *
+ * @param {Object} options - The options object
+ * @param {HttpSetup} options.http - HttpSetup
+ * @param {AbortSignal} [options.signal] - AbortSignal
+ * @param {IToasts} [options.toasts] - IToasts
+ *
+ * @returns mutation hook for updating Knowledge Base Entries
+ *
+ */
+export const useUpdateKnowledgeBaseEntries = ({
+ http,
+ signal,
+ toasts,
+}: UseUpdateKnowledgeBaseEntriesParams) => {
+ const invalidateKnowledgeBaseEntries = useInvalidateKnowledgeBaseEntries();
+
+ return useMutation(
+ BULK_UPDATE_KNOWLEDGE_BASE_ENTRY_MUTATION_KEY,
+ (updatedEntries: PerformKnowledgeBaseEntryBulkActionRequestBody['update']) => {
+ const body: PerformKnowledgeBaseEntryBulkActionRequestBody = {
+ update: updatedEntries,
+ };
+ return http.post(
+ ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BULK_ACTION,
+ {
+ body: JSON.stringify(body),
+ version: API_VERSIONS.internal.v1,
+ signal,
+ }
+ );
+ },
+ {
+ onError: (error: IHttpFetchError) => {
+ if (error.name !== 'AbortError') {
+ toasts?.addError(
+ error.body && error.body.message ? new Error(error.body.message) : error,
+ {
+ title: i18n.translate(
+ 'xpack.elasticAssistant.knowledgeBase.entries.updateErrorTitle',
+ {
+ defaultMessage: 'Error updating Knowledge Base Entries',
+ }
+ ),
+ }
+ );
+ }
+ },
+ onSettled: () => {
+ invalidateKnowledgeBaseEntries();
+ },
+ onSuccess: () => {
+ toasts?.addSuccess({
+ title: i18n.translate('xpack.elasticAssistant.knowledgeBase.entries.updateSuccessTitle', {
+ defaultMessage: 'Knowledge Base Entries updated successfully',
+ }),
+ });
+ },
+ }
+ );
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx
index ba6317329d350..6749b260fbfb9 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_knowledge_base_status.tsx
@@ -78,3 +78,20 @@ export const useInvalidateKnowledgeBaseStatus = () => {
});
}, [queryClient]);
};
+
+/**
+ * Helper for determining if Knowledge Base setup is complete.
+ *
+ * Note: Consider moving to API
+ *
+ * @param kbStatus ReadKnowledgeBaseResponse
+ */
+export const isKnowledgeBaseSetup = (kbStatus: ReadKnowledgeBaseResponse | undefined): boolean => {
+ return (
+ (kbStatus?.elser_exists &&
+ kbStatus?.esql_exists &&
+ kbStatus?.index_exists &&
+ kbStatus?.pipeline_exists) ??
+ false
+ );
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_setup_knowledge_base.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_setup_knowledge_base.test.tsx
index a252700ba744f..c08f6f93617fc 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_setup_knowledge_base.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_setup_knowledge_base.test.tsx
@@ -20,6 +20,7 @@ jest.mock('./api', () => {
};
});
jest.mock('./use_knowledge_base_status');
+jest.mock('./entries/use_knowledge_base_entries');
jest.mock('@tanstack/react-query', () => ({
useMutation: jest.fn().mockImplementation(async (queryKey, fn, opts) => {
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_setup_knowledge_base.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_setup_knowledge_base.tsx
index c27c97976e989..88a0e49ba724b 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_setup_knowledge_base.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/knowledge_base/use_setup_knowledge_base.tsx
@@ -11,6 +11,7 @@ import type { IToasts } from '@kbn/core-notifications-browser';
import { i18n } from '@kbn/i18n';
import { postKnowledgeBase } from './api';
import { useInvalidateKnowledgeBaseStatus } from './use_knowledge_base_status';
+import { useInvalidateKnowledgeBaseEntries } from './entries/use_knowledge_base_entries';
const SETUP_KNOWLEDGE_BASE_MUTATION_KEY = ['elastic-assistant', 'post-knowledge-base'];
@@ -31,6 +32,7 @@ export interface UseSetupKnowledgeBaseParams {
*/
export const useSetupKnowledgeBase = ({ http, toasts }: UseSetupKnowledgeBaseParams) => {
const invalidateKnowledgeBaseStatus = useInvalidateKnowledgeBaseStatus();
+ const invalidateKnowledgeBaseEntries = useInvalidateKnowledgeBaseEntries();
return useMutation(
SETUP_KNOWLEDGE_BASE_MUTATION_KEY,
@@ -53,6 +55,7 @@ export const useSetupKnowledgeBase = ({ http, toasts }: UseSetupKnowledgeBasePar
},
onSettled: () => {
invalidateKnowledgeBaseStatus();
+ invalidateKnowledgeBaseEntries();
},
}
);
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/badges/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/badges/index.tsx
index 3a93da1e6f72a..0a44664bd5d34 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/badges/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/badges/index.tsx
@@ -8,16 +8,19 @@
import { EuiBadge } from '@elastic/eui';
import React from 'react';
-export const BadgesColumn: React.FC<{ items: string[] | null | undefined; prefix: string }> =
- React.memo(({ items, prefix }) =>
- items && items.length > 0 ? (
-
- {items.map((c, idx) => (
-
- {c}
-
- ))}
-
- ) : null
- );
+export const BadgesColumn: React.FC<{
+ items: string[] | null | undefined;
+ prefix: string;
+ color?: string;
+}> = React.memo(({ items, prefix, color = 'hollow' }) =>
+ items && items.length > 0 ? (
+
+ {items.map((c, idx) => (
+
+ {c}
+
+ ))}
+
+ ) : null
+);
BadgesColumn.displayName = 'BadgesColumn';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/flyout/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/flyout/index.tsx
index f36591e5dbb0e..ac0109f31b9b7 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/flyout/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/flyout/index.tsx
@@ -22,7 +22,7 @@ import * as i18n from './translations';
interface Props {
children: React.ReactNode;
- title: string;
+ title?: string;
flyoutVisible: boolean;
onClose: () => void;
onSaveCancelled: () => void;
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/inline_actions/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/inline_actions/index.tsx
index 6e955dd554d39..f89ad5912a60a 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/inline_actions/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/common/components/assistant_settings_management/inline_actions/index.tsx
@@ -10,51 +10,60 @@ import { useCallback } from 'react';
import * as i18n from './translations';
interface Props {
- disabled?: boolean;
+ isEditEnabled?: (rowItem: T) => boolean;
+ isDeleteEnabled?: (rowItem: T) => boolean;
onDelete?: (rowItem: T) => void;
onEdit?: (rowItem: T) => void;
}
export const useInlineActions = () => {
- const getInlineActions = useCallback(({ disabled = false, onDelete, onEdit }: Props) => {
- const handleEdit = (rowItem: T) => {
- onEdit?.(rowItem);
- };
+ const getInlineActions = useCallback(
+ ({
+ isEditEnabled = () => false,
+ isDeleteEnabled = () => false,
+ onDelete,
+ onEdit,
+ }: Props) => {
+ const handleEdit = (rowItem: T) => {
+ onEdit?.(rowItem);
+ };
- const handleDelete = (rowItem: T) => {
- onDelete?.(rowItem);
- };
+ const handleDelete = (rowItem: T) => {
+ onDelete?.(rowItem);
+ };
- const actions: EuiTableActionsColumnType = {
- name: i18n.ACTIONS_BUTTON,
- actions: [
- {
- name: i18n.EDIT_BUTTON,
- description: i18n.EDIT_BUTTON,
- icon: 'pencil',
- type: 'icon',
- onClick: (rowItem: T) => {
- handleEdit(rowItem);
+ const actions: EuiTableActionsColumnType = {
+ name: i18n.ACTIONS_BUTTON,
+ actions: [
+ {
+ name: i18n.EDIT_BUTTON,
+ description: i18n.EDIT_BUTTON,
+ icon: 'pencil',
+ type: 'icon',
+ onClick: (rowItem: T) => {
+ handleEdit(rowItem);
+ },
+ enabled: isEditEnabled,
+ available: () => onEdit != null,
},
- enabled: () => !disabled,
- available: () => onEdit != null,
- },
- {
- name: i18n.DELETE_BUTTON,
- description: i18n.DELETE_BUTTON,
- icon: 'trash',
- type: 'icon',
- onClick: (rowItem: T) => {
- handleDelete(rowItem);
+ {
+ name: i18n.DELETE_BUTTON,
+ description: i18n.DELETE_BUTTON,
+ icon: 'trash',
+ type: 'icon',
+ onClick: (rowItem: T) => {
+ handleDelete(rowItem);
+ },
+ enabled: isDeleteEnabled,
+ available: () => onDelete != null,
+ color: 'danger',
},
- enabled: ({ isDefault }: { isDefault?: boolean }) => !isDefault && !disabled,
- available: () => onDelete != null,
- color: 'danger',
- },
- ],
- };
- return actions;
- }, []);
+ ],
+ };
+ return actions;
+ },
+ []
+ );
return getInlineActions;
};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/index.tsx
index f8818f5faab25..368b7d3d2c8b6 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/index.tsx
@@ -260,6 +260,8 @@ const ConversationSettingsManagementComponent: React.FC = ({
const columns = useMemo(
() =>
getColumns({
+ isDeleteEnabled: (rowItem: ConversationTableItem) => rowItem.isDefault !== true,
+ isEditEnabled: () => true,
onDeleteActionClicked,
onEditActionClicked,
}),
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.test.tsx
index 1c293cf061c29..797bde3466223 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.test.tsx
@@ -34,6 +34,8 @@ describe('useConversationsTable', () => {
it('should return columns', () => {
const { result } = renderHook(() => useConversationsTable());
const columns = result.current.getColumns({
+ isDeleteEnabled: jest.fn(),
+ isEditEnabled: jest.fn(),
onDeleteActionClicked: jest.fn(),
onEditActionClicked: jest.fn(),
});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.tsx
index e9c1cead27d66..001bdfd559003 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.tsx
@@ -38,9 +38,13 @@ export const useConversationsTable = () => {
const getActions = useInlineActions();
const getColumns = useCallback(
({
+ isDeleteEnabled,
+ isEditEnabled,
onDeleteActionClicked,
onEditActionClicked,
}: {
+ isDeleteEnabled: (conversation: ConversationTableItem) => boolean;
+ isEditEnabled: (conversation: ConversationTableItem) => boolean;
onDeleteActionClicked: (conversation: ConversationTableItem) => void;
onEditActionClicked: (conversation: ConversationTableItem) => void;
}): Array> => {
@@ -91,6 +95,8 @@ export const useConversationsTable = () => {
width: '120px',
align: 'center',
...getActions({
+ isDeleteEnabled,
+ isEditEnabled,
onDelete: onDeleteActionClicked,
onEdit: onEditActionClicked,
}),
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx
index 43f637b1769f3..c52d94138b839 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx
@@ -90,9 +90,12 @@ const AssistantComponent: React.FC = ({
getLastConversationId,
http,
promptContexts,
+ setCurrentUserAvatar,
setLastConversationId,
} = useAssistantContext();
+ setCurrentUserAvatar(currentUserAvatar);
+
const [selectedPromptContexts, setSelectedPromptContexts] = useState<
Record
>({});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/index.tsx
index 69ec81547912e..ac4488a50b79b 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/index.tsx
@@ -204,7 +204,13 @@ const SystemPromptSettingsManagementComponent = ({ connectors, defaultConnector
const columns = useMemo(
() =>
- getColumns({ isActionsDisabled: isTableLoading, onEditActionClicked, onDeleteActionClicked }),
+ getColumns({
+ isActionsDisabled: isTableLoading,
+ onEditActionClicked,
+ onDeleteActionClicked,
+ isDeleteEnabled: (prompt: PromptResponse) => prompt.isDefault !== true,
+ isEditEnabled: () => true,
+ }),
[getColumns, isTableLoading, onEditActionClicked, onDeleteActionClicked]
);
const systemPromptListItems = useMemo(
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.test.tsx
index e677001e2d38b..220e150ec8647 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.test.tsx
@@ -66,6 +66,8 @@ describe('useSystemPromptTable', () => {
const onDeleteActionClicked = jest.fn();
const columns = result.current.getColumns({
isActionsDisabled: false,
+ isDeleteEnabled: jest.fn(),
+ isEditEnabled: jest.fn(),
onEditActionClicked,
onDeleteActionClicked,
});
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.tsx
index d2b63e9abdbd5..26eb003d86bf5 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.tsx
@@ -25,10 +25,14 @@ export const useSystemPromptTable = () => {
const getColumns = useCallback(
({
isActionsDisabled,
+ isDeleteEnabled,
+ isEditEnabled,
onEditActionClicked,
onDeleteActionClicked,
}: {
isActionsDisabled: boolean;
+ isDeleteEnabled: (conversation: SystemPromptTableItem) => boolean;
+ isEditEnabled: (conversation: SystemPromptTableItem) => boolean;
onEditActionClicked: (prompt: SystemPromptTableItem) => void;
onDeleteActionClicked: (prompt: SystemPromptTableItem) => void;
}): Array> => [
@@ -79,6 +83,8 @@ export const useSystemPromptTable = () => {
align: 'center',
width: '120px',
...getActions({
+ isDeleteEnabled,
+ isEditEnabled,
onDelete: onDeleteActionClicked,
onEdit: onEditActionClicked,
}),
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/index.tsx
index 0f7b6df8d1893..16389b331175d 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/index.tsx
@@ -148,6 +148,8 @@ const QuickPromptSettingsManagementComponent = () => {
basePromptContexts,
onEditActionClicked,
onDeleteActionClicked,
+ isDeleteEnabled: (prompt: PromptResponse) => prompt.isDefault !== true,
+ isEditEnabled: () => true,
});
const { onTableChange, pagination, sorting } = useSessionPagination({
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.test.tsx
index 6732b93a5b5d9..ae91a8bb971c6 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.test.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.test.tsx
@@ -12,6 +12,7 @@ import { MOCK_QUICK_PROMPTS } from '../../../mock/quick_prompt';
import { mockPromptContexts } from '../../../mock/prompt_context';
import { PromptResponse } from '@kbn/elastic-assistant-common';
+const mockIsEditEnabled = jest.fn();
const mockOnEditActionClicked = jest.fn();
const mockOnDeleteActionClicked = jest.fn();
@@ -20,6 +21,8 @@ describe('useQuickPromptTable', () => {
const props = {
isActionsDisabled: false,
basePromptContexts: mockPromptContexts,
+ isDeleteEnabled: (prompt: PromptResponse) => prompt.isDefault !== true,
+ isEditEnabled: mockIsEditEnabled,
onEditActionClicked: mockOnEditActionClicked,
onDeleteActionClicked: mockOnDeleteActionClicked,
};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.tsx
index 0f2c6a88bc3d7..e9de769e3f735 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.tsx
@@ -19,11 +19,15 @@ export const useQuickPromptTable = () => {
const getColumns = useCallback(
({
isActionsDisabled,
+ isDeleteEnabled,
+ isEditEnabled,
basePromptContexts,
onEditActionClicked,
onDeleteActionClicked,
}: {
isActionsDisabled: boolean;
+ isDeleteEnabled: (prompt: PromptResponse) => boolean;
+ isEditEnabled: (prompt: PromptResponse) => boolean;
basePromptContexts: PromptContextTemplate[];
onEditActionClicked: (prompt: PromptResponse, color?: string) => void;
onDeleteActionClicked: (prompt: PromptResponse) => void;
@@ -74,6 +78,8 @@ export const useQuickPromptTable = () => {
align: 'center',
width: '120px',
...getActions({
+ isDeleteEnabled,
+ isEditEnabled,
onDelete: onDeleteActionClicked,
onEdit: onEditActionClicked,
}),
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx
index 4217a3d9dc2b8..75516eaf907b2 100644
--- a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx
+++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx
@@ -96,6 +96,7 @@ export interface UseAssistantContext {
docLinks: Omit;
basePath: string;
baseConversations: Record;
+ currentUserAvatar?: UserAvatar;
getComments: GetAssistantMessages;
http: HttpSetup;
knowledgeBase: KnowledgeBaseConfig;
@@ -106,6 +107,7 @@ export interface UseAssistantContext {
registerPromptContext: RegisterPromptContext;
selectedSettingsTab: SettingsTabs | null;
setAssistantStreamingEnabled: React.Dispatch>;
+ setCurrentUserAvatar: React.Dispatch>;
setKnowledgeBase: React.Dispatch>;
setLastConversationId: React.Dispatch>;
setSelectedSettingsTab: React.Dispatch>;
@@ -218,6 +220,11 @@ export const AssistantProvider: React.FC = ({
*/
const [showAssistantOverlay, setShowAssistantOverlay] = useState(() => {});
+ /**
+ * Current User Avatar
+ */
+ const [currentUserAvatar, setCurrentUserAvatar] = useState();
+
/**
* Settings State
*/
@@ -250,6 +257,7 @@ export const AssistantProvider: React.FC = ({
augmentMessageCodeBlocks,
basePath,
basePromptContexts,
+ currentUserAvatar,
docLinks,
getComments,
http,
@@ -263,6 +271,7 @@ export const AssistantProvider: React.FC = ({
assistantStreamingEnabled: localStorageStreaming ?? true,
setAssistantStreamingEnabled: setLocalStorageStreaming,
setKnowledgeBase: setLocalStorageKnowledgeBase,
+ setCurrentUserAvatar,
setSelectedSettingsTab,
setShowAssistantOverlay,
setTraceOptions: setSessionStorageTraceOptions,
@@ -286,6 +295,7 @@ export const AssistantProvider: React.FC = ({
augmentMessageCodeBlocks,
basePath,
basePromptContexts,
+ currentUserAvatar,
docLinks,
getComments,
http,
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/alerts_range.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/alerts_range.tsx
new file mode 100644
index 0000000000000..152f0a91a7d04
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/alerts_range.tsx
@@ -0,0 +1,60 @@
+/*
+ * 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 { EuiRange, useGeneratedHtmlId } from '@elastic/eui';
+import { css } from '@emotion/react';
+import React from 'react';
+import {
+ MAX_LATEST_ALERTS,
+ MIN_LATEST_ALERTS,
+ TICK_INTERVAL,
+} from '../alerts/settings/alerts_settings';
+import { KnowledgeBaseConfig } from '../assistant/types';
+import { ALERTS_RANGE } from './translations';
+
+interface Props {
+ knowledgeBase: KnowledgeBaseConfig;
+ setUpdatedKnowledgeBaseSettings: React.Dispatch>;
+ compressed?: boolean;
+}
+
+const MAX_ALERTS_RANGE_WIDTH = 649; // px
+
+export const AlertsRange: React.FC = React.memo(
+ ({ knowledgeBase, setUpdatedKnowledgeBaseSettings, compressed = true }) => {
+ const inputRangeSliderId = useGeneratedHtmlId({ prefix: 'inputRangeSlider' });
+
+ return (
+
+ setUpdatedKnowledgeBaseSettings({
+ ...knowledgeBase,
+ latestAlerts: Number(e.currentTarget.value),
+ })
+ }
+ showTicks
+ step={TICK_INTERVAL}
+ value={knowledgeBase.latestAlerts}
+ css={css`
+ max-inline-size: ${MAX_ALERTS_RANGE_WIDTH}px;
+ & .euiRangeTrack {
+ margin-inline-start: 0;
+ margin-inline-end: 0;
+ }
+ `}
+ />
+ );
+ }
+);
+
+AlertsRange.displayName = 'AlertsRange';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/const.ts b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/const.ts
new file mode 100644
index 0000000000000..3cfd0cf3b4205
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/const.ts
@@ -0,0 +1,9 @@
+/*
+ * 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.
+ */
+export const ESQL_RESOURCE = 'esql';
+export const KNOWLEDGE_BASE_INDEX_PATTERN_OLD = '.kibana-elastic-ai-assistant-kb';
+export const KNOWLEDGE_BASE_INDEX_PATTERN = '.kibana-elastic-ai-assistant-knowledge-base-(SPACE)';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management.tsx
deleted file mode 100644
index 0f0a90b05b0de..0000000000000
--- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management.tsx
+++ /dev/null
@@ -1,272 +0,0 @@
-/*
- * 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 React, { useCallback, useMemo, useState } from 'react';
-import {
- EuiFormRow,
- EuiText,
- EuiHorizontalRule,
- EuiSpacer,
- EuiLink,
- EuiFlexGroup,
- EuiFlexItem,
- EuiHealth,
- EuiButtonEmpty,
- EuiPanel,
- EuiToolTip,
-} from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n-react';
-import { css } from '@emotion/react';
-
-import { AlertsSettings } from '../alerts/settings/alerts_settings';
-import { useAssistantContext } from '../assistant_context';
-import * as i18n from './translations';
-import { useKnowledgeBaseStatus } from '../assistant/api/knowledge_base/use_knowledge_base_status';
-import { useSetupKnowledgeBase } from '../assistant/api/knowledge_base/use_setup_knowledge_base';
-import {
- useSettingsUpdater,
- DEFAULT_CONVERSATIONS,
- DEFAULT_PROMPTS,
-} from '../assistant/settings/use_settings_updater/use_settings_updater';
-import { AssistantSettingsBottomBar } from '../assistant/settings/assistant_settings_bottom_bar';
-import { SETTINGS_UPDATED_TOAST_TITLE } from '../assistant/settings/translations';
-import { SETUP_KNOWLEDGE_BASE_BUTTON_TOOLTIP } from './translations';
-
-const ESQL_RESOURCE = 'esql';
-const KNOWLEDGE_BASE_INDEX_PATTERN = '.kibana-elastic-ai-assistant-knowledge-base-(SPACE)';
-
-/**
- * Knowledge Base Settings -- set up the Knowledge Base and configure RAG on alerts
- */
-export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
- const { http, toasts } = useAssistantContext();
- const [hasPendingChanges, setHasPendingChanges] = useState(false);
-
- const { knowledgeBase, setUpdatedKnowledgeBaseSettings, resetSettings, saveSettings } =
- useSettingsUpdater(
- DEFAULT_CONVERSATIONS, // Knowledge Base settings do not require conversations
- DEFAULT_PROMPTS, // Knowledge Base settings do not require prompts
- false, // Knowledge Base settings do not require prompts
- false // Knowledge Base settings do not require conversations
- );
-
- const handleSave = useCallback(
- async (param?: { callback?: () => void }) => {
- await saveSettings();
- toasts?.addSuccess({
- iconType: 'check',
- title: SETTINGS_UPDATED_TOAST_TITLE,
- });
- setHasPendingChanges(false);
- param?.callback?.();
- },
- [saveSettings, toasts]
- );
-
- const handleUpdateKnowledgeBaseSettings = useCallback(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (updatedKnowledgebase: any) => {
- setHasPendingChanges(true);
- setUpdatedKnowledgeBaseSettings(updatedKnowledgebase);
- },
- [setUpdatedKnowledgeBaseSettings]
- );
-
- const onCancelClick = useCallback(() => {
- resetSettings();
- setHasPendingChanges(false);
- }, [resetSettings]);
-
- const onSaveButtonClicked = useCallback(() => {
- handleSave();
- }, [handleSave]);
-
- const {
- data: kbStatus,
- isLoading,
- isFetching,
- } = useKnowledgeBaseStatus({ http, resource: ESQL_RESOURCE });
- const { mutate: setupKB, isLoading: isSettingUpKB } = useSetupKnowledgeBase({ http, toasts });
-
- // Resource enabled state
- const isElserEnabled = kbStatus?.elser_exists ?? false;
- const isESQLEnabled = kbStatus?.esql_exists ?? false;
- const isKnowledgeBaseSetup =
- (isElserEnabled && isESQLEnabled && kbStatus?.index_exists && kbStatus?.pipeline_exists) ??
- false;
- const isSetupInProgress = kbStatus?.is_setup_in_progress ?? false;
- const isSetupAvailable = kbStatus?.is_setup_available ?? false;
-
- // Resource availability state
- const isLoadingKb = isLoading || isFetching || isSettingUpKB || isSetupInProgress;
-
- // Calculated health state for EuiHealth component
- const elserHealth = isElserEnabled ? 'success' : 'subdued';
- const knowledgeBaseHealth = isKnowledgeBaseSetup ? 'success' : 'subdued';
- const esqlHealth = isESQLEnabled ? 'success' : 'subdued';
-
- //////////////////////////////////////////////////////////////////////////////////////////
- // Main `Knowledge Base` setup button
- const onSetupKnowledgeBaseButtonClick = useCallback(() => {
- setupKB(ESQL_RESOURCE);
- }, [setupKB]);
-
- const toolTipContent = !isSetupAvailable ? SETUP_KNOWLEDGE_BASE_BUTTON_TOOLTIP : undefined;
-
- const setupKnowledgeBaseButton = useMemo(() => {
- return isKnowledgeBaseSetup ? (
- <>>
- ) : (
-
-
- {i18n.SETUP_KNOWLEDGE_BASE_BUTTON}
-
-
- );
- }, [
- isKnowledgeBaseSetup,
- isLoadingKb,
- isSetupAvailable,
- onSetupKnowledgeBaseButtonClick,
- toolTipContent,
- ]);
-
- //////////////////////////////////////////////////////////////////////////////////////////
- // Knowledge Base Resource
- const knowledgeBaseDescription = useMemo(() => {
- return isKnowledgeBaseSetup ? (
-
- {i18n.KNOWLEDGE_BASE_DESCRIPTION_INSTALLED(KNOWLEDGE_BASE_INDEX_PATTERN)}
-
- ) : (
- {i18n.KNOWLEDGE_BASE_DESCRIPTION}
- );
- }, [isKnowledgeBaseSetup]);
-
- //////////////////////////////////////////////////////////////////////////////////////////
- // ESQL Resource
-
- const esqlDescription = useMemo(() => {
- return isESQLEnabled ? (
- {i18n.ESQL_DESCRIPTION_INSTALLED}
- ) : (
- {i18n.ESQL_DESCRIPTION}
- );
- }, [isESQLEnabled]);
-
- return (
-
-
-
- {i18n.KNOWLEDGE_BASE_DOCUMENTATION}
-
- ),
- }}
- />
-
-
-
-
- {setupKnowledgeBaseButton}
-
-
-
-
-
-
- {i18n.KNOWLEDGE_BASE_ELSER_LABEL}
-
-
-
-
-
-
-
- {i18n.KNOWLEDGE_BASE_LABEL}
-
- {knowledgeBaseDescription}
-
-
-
-
-
- {i18n.ESQL_LABEL}
-
- {esqlDescription}
-
-
-
-
-
-
-
-
-
-
-
- );
-});
-
-KnowledgeBaseSettingsManagement.displayName = 'KnowledgeBaseSettingsManagement';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/add_entry_button.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/add_entry_button.tsx
new file mode 100644
index 0000000000000..5b3ec4562d086
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/add_entry_button.tsx
@@ -0,0 +1,85 @@
+/*
+ * 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 {
+ EuiButton,
+ EuiIcon,
+ EuiPopover,
+ EuiContextMenuItem,
+ EuiContextMenuPanel,
+} from '@elastic/eui';
+import React, { useCallback, useState } from 'react';
+import * as i18n from './translations';
+
+interface Props {
+ isDocumentAvailable?: boolean;
+ isIndexAvailable?: boolean;
+ onDocumentClicked?: () => void;
+ onIndexClicked?: () => void;
+}
+
+export const AddEntryButton: React.FC = React.memo(
+ ({
+ isDocumentAvailable = true,
+ isIndexAvailable = true,
+ onDocumentClicked,
+ onIndexClicked,
+ }: Props) => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const closePopover = useCallback(() => setIsPopoverOpen(false), []);
+ const handleIndexClicked = useCallback(() => {
+ closePopover();
+ onIndexClicked?.();
+ }, [closePopover, onIndexClicked]);
+
+ const handleDocumentClicked = useCallback(() => {
+ closePopover();
+ onDocumentClicked?.();
+ }, [closePopover, onDocumentClicked]);
+
+ const onButtonClick = useCallback(() => setIsPopoverOpen((prevState) => !prevState), []);
+
+ const items = [
+
+ {i18n.INDEX}
+ ,
+
+ {i18n.DOCUMENT}
+ ,
+ ];
+ return onIndexClicked || onDocumentClicked ? (
+
+
+ {i18n.NEW}
+
+ }
+ isOpen={isPopoverOpen}
+ closePopover={closePopover}
+ anchorPosition="downLeft"
+ >
+
+
+ ) : null;
+ }
+);
+
+AddEntryButton.displayName = 'AddEntryButton';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/document_entry_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/document_entry_editor.tsx
new file mode 100644
index 0000000000000..850c8b29d063a
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/document_entry_editor.tsx
@@ -0,0 +1,136 @@
+/*
+ * 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 {
+ EuiCheckbox,
+ EuiFieldText,
+ EuiForm,
+ EuiFormRow,
+ EuiMarkdownEditor,
+ EuiSuperSelect,
+ EuiIcon,
+ EuiText,
+} from '@elastic/eui';
+import React, { useCallback } from 'react';
+import { DocumentEntry } from '@kbn/elastic-assistant-common';
+import * as i18n from './translations';
+
+interface Props {
+ entry?: DocumentEntry;
+ setEntry: React.Dispatch>>;
+}
+
+export const DocumentEntryEditor: React.FC = React.memo(({ entry, setEntry }) => {
+ // Name
+ const setName = useCallback(
+ (e) => setEntry((prevEntry) => ({ ...prevEntry, name: e.target.value })),
+ [setEntry]
+ );
+
+ // Sharing
+ const setSharingOptions = useCallback(
+ (value) =>
+ setEntry((prevEntry) => ({
+ ...prevEntry,
+ users: value === i18n.SHARING_GLOBAL_OPTION_LABEL ? [] : undefined,
+ })),
+ [setEntry]
+ );
+ // TODO: KB-RBAC Disable global option if no RBAC
+ const sharingOptions = [
+ {
+ value: i18n.SHARING_PRIVATE_OPTION_LABEL,
+ inputDisplay: (
+
+
+ {i18n.SHARING_PRIVATE_OPTION_LABEL}
+
+ ),
+ },
+ {
+ value: i18n.SHARING_GLOBAL_OPTION_LABEL,
+ inputDisplay: (
+
+
+ {i18n.SHARING_GLOBAL_OPTION_LABEL}
+
+ ),
+ },
+ ];
+ const selectedSharingOption =
+ entry?.users?.length === 0 ? sharingOptions[1].value : sharingOptions[0].value;
+
+ // Text / markdown
+ const setMarkdownValue = useCallback(
+ (value: string) => {
+ setEntry((prevEntry) => ({ ...prevEntry, text: value }));
+ },
+ [setEntry]
+ );
+
+ // Required checkbox
+ const onRequiredKnowledgeChanged = useCallback(
+ (e: React.ChangeEvent) => {
+ setEntry((prevEntry) => ({ ...prevEntry, required: e.target.checked }));
+ },
+ [setEntry]
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+DocumentEntryEditor.displayName = 'DocumentEntryEditor';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/helpers.ts b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/helpers.ts
new file mode 100644
index 0000000000000..3d522ab975f3c
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/helpers.ts
@@ -0,0 +1,36 @@
+/*
+ * 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 {
+ DocumentEntryType,
+ KnowledgeBaseEntryCreateProps,
+ KnowledgeBaseEntryResponse,
+} from '@kbn/elastic-assistant-common';
+import { z } from '@kbn/zod';
+
+export const isEsqlSystemEntry = (
+ entry: KnowledgeBaseEntryResponse
+): entry is KnowledgeBaseEntryResponse & {
+ type: DocumentEntryType;
+ kbResource: 'esql';
+} => {
+ return entry.type === DocumentEntryType.value && entry.kbResource === 'esql';
+};
+
+export const isKnowledgeBaseEntryCreateProps = (
+ entry: unknown
+): entry is z.infer => {
+ const result = KnowledgeBaseEntryCreateProps.safeParse(entry);
+ return result.success;
+};
+
+export const isKnowledgeBaseEntryResponse = (
+ entry: unknown
+): entry is z.infer => {
+ const result = KnowledgeBaseEntryResponse.safeParse(entry);
+ return result.success;
+};
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx
new file mode 100644
index 0000000000000..d970b76e98bd1
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx
@@ -0,0 +1,300 @@
+/*
+ * 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 {
+ EuiInMemoryTable,
+ EuiLink,
+ EuiPanel,
+ EuiSearchBarProps,
+ EuiSpacer,
+ EuiText,
+} from '@elastic/eui';
+import React, { useCallback, useMemo, useState } from 'react';
+import { FormattedMessage } from '@kbn/i18n-react';
+import {
+ DocumentEntry,
+ DocumentEntryType,
+ IndexEntry,
+ IndexEntryType,
+ KnowledgeBaseEntryCreateProps,
+ KnowledgeBaseEntryResponse,
+} from '@kbn/elastic-assistant-common';
+import { AlertsSettingsManagement } from '../../alerts/settings/alerts_settings_management';
+import { useKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_knowledge_base_entries';
+import { useAssistantContext } from '../../assistant_context';
+import { useKnowledgeBaseTable } from './use_knowledge_base_table';
+import { AssistantSettingsBottomBar } from '../../assistant/settings/assistant_settings_bottom_bar';
+import {
+ useSettingsUpdater,
+ DEFAULT_CONVERSATIONS,
+ DEFAULT_PROMPTS,
+} from '../../assistant/settings/use_settings_updater/use_settings_updater';
+import { AddEntryButton } from './add_entry_button';
+import * as i18n from './translations';
+import { Flyout } from '../../assistant/common/components/assistant_settings_management/flyout';
+import { useFlyoutModalVisibility } from '../../assistant/common/components/assistant_settings_management/flyout/use_flyout_modal_visibility';
+import { IndexEntryEditor } from './index_entry_editor';
+import { DocumentEntryEditor } from './document_entry_editor';
+import { KnowledgeBaseSettings } from '../knowledge_base_settings';
+import { SetupKnowledgeBaseButton } from '../setup_knowledge_base_button';
+import { useDeleteKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_delete_knowledge_base_entries';
+import {
+ isEsqlSystemEntry,
+ isKnowledgeBaseEntryCreateProps,
+ isKnowledgeBaseEntryResponse,
+} from './helpers';
+import { useCreateKnowledgeBaseEntry } from '../../assistant/api/knowledge_base/entries/use_create_knowledge_base_entry';
+import { useUpdateKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_update_knowledge_base_entries';
+import { SETTINGS_UPDATED_TOAST_TITLE } from '../../assistant/settings/translations';
+
+export const KnowledgeBaseSettingsManagement: React.FC = React.memo(() => {
+ const {
+ assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault },
+ http,
+ toasts,
+ } = useAssistantContext();
+ const [hasPendingChanges, setHasPendingChanges] = useState(false);
+
+ // Only needed for legacy settings management
+ const { knowledgeBase, setUpdatedKnowledgeBaseSettings, resetSettings, saveSettings } =
+ useSettingsUpdater(
+ DEFAULT_CONVERSATIONS, // Knowledge Base settings do not require conversations
+ DEFAULT_PROMPTS, // Knowledge Base settings do not require prompts
+ false, // Knowledge Base settings do not require conversations
+ false // Knowledge Base settings do not require prompts
+ );
+
+ const handleUpdateKnowledgeBaseSettings = useCallback(
+ (updatedKnowledgeBase) => {
+ setHasPendingChanges(true);
+ setUpdatedKnowledgeBaseSettings(updatedKnowledgeBase);
+ },
+ [setUpdatedKnowledgeBaseSettings]
+ );
+
+ const handleSave = useCallback(
+ async (param?: { callback?: () => void }) => {
+ await saveSettings();
+ toasts?.addSuccess({
+ iconType: 'check',
+ title: SETTINGS_UPDATED_TOAST_TITLE,
+ });
+ setHasPendingChanges(false);
+ param?.callback?.();
+ },
+ [saveSettings, toasts]
+ );
+
+ const onCancelClick = useCallback(() => {
+ resetSettings();
+ setHasPendingChanges(false);
+ }, [resetSettings]);
+
+ const onSaveButtonClicked = useCallback(() => {
+ handleSave();
+ }, [handleSave]);
+
+ const { isFlyoutOpen: isFlyoutVisible, openFlyout, closeFlyout } = useFlyoutModalVisibility();
+
+ const [selectedEntry, setSelectedEntry] =
+ useState>();
+
+ // CRUD API accessors
+ const { mutate: createEntry, isLoading: isCreatingEntry } = useCreateKnowledgeBaseEntry({
+ http,
+ toasts,
+ });
+ const { mutate: updateEntries, isLoading: isUpdatingEntries } = useUpdateKnowledgeBaseEntries({
+ http,
+ toasts,
+ });
+ const { mutate: deleteEntry, isLoading: isDeletingEntries } = useDeleteKnowledgeBaseEntries({
+ http,
+ toasts,
+ });
+ const isModifyingEntry = isCreatingEntry || isUpdatingEntries || isDeletingEntries;
+
+ // Flyout Save/Cancel Actions
+ const onSaveConfirmed = useCallback(() => {
+ if (isKnowledgeBaseEntryCreateProps(selectedEntry)) {
+ createEntry(selectedEntry);
+ closeFlyout();
+ } else if (isKnowledgeBaseEntryResponse(selectedEntry)) {
+ updateEntries([selectedEntry]);
+ closeFlyout();
+ }
+ }, [closeFlyout, selectedEntry, createEntry, updateEntries]);
+
+ const onSaveCancelled = useCallback(() => {
+ setSelectedEntry(undefined);
+ closeFlyout();
+ }, [closeFlyout]);
+
+ const { data: entries } = useKnowledgeBaseEntries({
+ http,
+ toasts,
+ enabled: enableKnowledgeBaseByDefault,
+ });
+ const { getColumns } = useKnowledgeBaseTable();
+ const columns = useMemo(
+ () =>
+ getColumns({
+ onEntryNameClicked: ({ id }: KnowledgeBaseEntryResponse) => {
+ const entry = entries.data.find((e) => e.id === id);
+ setSelectedEntry(entry);
+ openFlyout();
+ },
+ onSpaceNameClicked: ({ namespace }: KnowledgeBaseEntryResponse) => {
+ openFlyout();
+ },
+ isDeleteEnabled: (entry: KnowledgeBaseEntryResponse) => {
+ return !isEsqlSystemEntry(entry);
+ },
+ onDeleteActionClicked: ({ id }: KnowledgeBaseEntryResponse) => {
+ deleteEntry({ ids: [id] });
+ },
+ isEditEnabled: (entry: KnowledgeBaseEntryResponse) => {
+ return !isEsqlSystemEntry(entry);
+ },
+ onEditActionClicked: ({ id }: KnowledgeBaseEntryResponse) => {
+ const entry = entries.data.find((e) => e.id === id);
+ setSelectedEntry(entry);
+ openFlyout();
+ },
+ }),
+ [deleteEntry, entries.data, getColumns, openFlyout]
+ );
+
+ const onDocumentClicked = useCallback(() => {
+ setSelectedEntry({ type: DocumentEntryType.value, kbResource: 'user', source: 'user' });
+ openFlyout();
+ }, [openFlyout]);
+
+ const onIndexClicked = useCallback(() => {
+ setSelectedEntry({ type: IndexEntryType.value });
+ openFlyout();
+ }, [openFlyout]);
+
+ const search: EuiSearchBarProps = useMemo(
+ () => ({
+ toolsRight: (
+
+ ),
+ box: {
+ incremental: true,
+ placeholder: i18n.SEARCH_PLACEHOLDER,
+ },
+ filters: [],
+ }),
+ [onDocumentClicked, onIndexClicked]
+ );
+
+ const flyoutTitle = useMemo(() => {
+ // @ts-expect-error TS doesn't understand that selectedEntry is a partial
+ if (selectedEntry?.id != null) {
+ return selectedEntry.type === DocumentEntryType.value
+ ? i18n.EDIT_DOCUMENT_FLYOUT_TITLE
+ : i18n.EDIT_INDEX_FLYOUT_TITLE;
+ }
+ return selectedEntry?.type === DocumentEntryType.value
+ ? i18n.NEW_DOCUMENT_FLYOUT_TITLE
+ : i18n.NEW_INDEX_FLYOUT_TITLE;
+ }, [selectedEntry]);
+
+ if (!enableKnowledgeBaseByDefault) {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ const sorting = {
+ sort: {
+ field: 'name',
+ direction: 'desc' as const,
+ },
+ };
+
+ return (
+ <>
+
+
+
+ {i18n.KNOWLEDGE_BASE_DOCUMENTATION}
+
+ ),
+ }}
+ />
+
+
+
+
+
+
+
+
+
+ <>
+ {selectedEntry?.type === DocumentEntryType.value ? (
+ >>
+ }
+ />
+ ) : (
+ >>
+ }
+ />
+ )}
+ >
+
+ >
+ );
+});
+
+KnowledgeBaseSettingsManagement.displayName = 'KnowledgeBaseSettingsManagement';
diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index_entry_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index_entry_editor.tsx
new file mode 100644
index 0000000000000..97ae01ad2edd4
--- /dev/null
+++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index_entry_editor.tsx
@@ -0,0 +1,188 @@
+/*
+ * 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 {
+ EuiComboBox,
+ EuiFieldText,
+ EuiForm,
+ EuiFormRow,
+ EuiComboBoxOptionOption,
+ EuiText,
+ EuiIcon,
+ EuiSuperSelect,
+} from '@elastic/eui';
+import React, { useCallback } from 'react';
+import { IndexEntry } from '@kbn/elastic-assistant-common';
+import * as i18n from './translations';
+
+interface Props {
+ entry?: IndexEntry;
+ setEntry: React.Dispatch>>;
+}
+
+export const IndexEntryEditor: React.FC = React.memo(({ entry, setEntry }) => {
+ // Name
+ const setName = useCallback(
+ (e) => setEntry((prevEntry) => ({ ...prevEntry, name: e.target.value })),
+ [setEntry]
+ );
+
+ // Sharing
+ const setSharingOptions = useCallback(
+ (value) =>
+ setEntry((prevEntry) => ({
+ ...prevEntry,
+ users: value === i18n.SHARING_GLOBAL_OPTION_LABEL ? [] : undefined,
+ })),
+ [setEntry]
+ );
+ // TODO: KB-RBAC Disable global option if no RBAC
+ const sharingOptions = [
+ {
+ value: i18n.SHARING_PRIVATE_OPTION_LABEL,
+ inputDisplay: (
+
+
+ {i18n.SHARING_PRIVATE_OPTION_LABEL}
+
+ ),
+ },
+ {
+ value: i18n.SHARING_GLOBAL_OPTION_LABEL,
+ inputDisplay: (
+
+
+ {i18n.SHARING_GLOBAL_OPTION_LABEL}
+
+ ),
+ },
+ ];
+ const selectedSharingOption =
+ entry?.users?.length === 0 ? sharingOptions[1].value : sharingOptions[0].value;
+
+ // Index
+ const setIndex = useCallback(
+ (e: Array>) =>
+ setEntry((prevEntry) => ({ ...prevEntry, index: e[0].value })),
+ [setEntry]
+ );
+
+ const onCreateOption = (searchValue: string) => {
+ const normalizedSearchValue = searchValue.trim().toLowerCase();
+
+ if (!normalizedSearchValue) {
+ return;
+ }
+
+ const newOption: EuiComboBoxOptionOption = {
+ label: searchValue,
+ value: searchValue,
+ };
+
+ setIndex([newOption]);
+ };
+
+ // Field
+ const setField = useCallback(
+ (e) => setEntry((prevEntry) => ({ ...prevEntry, field: e.target.value })),
+ [setEntry]
+ );
+
+ // Description
+ const setDescription = useCallback(
+ (e) => setEntry((prevEntry) => ({ ...prevEntry, description: e.target.value })),
+ [setEntry]
+ );
+
+ // Query Description
+ const setQueryDescription = useCallback(
+ (e) => setEntry((prevEntry) => ({ ...prevEntry, queryDescription: e.target.value })),
+ [setEntry]
+ );
+
+ return (
+
+