From e03f8f066c44bbbf6d0e9fc0c4bc5c72f79f3471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Mon, 19 Dec 2022 11:33:58 +0100 Subject: [PATCH 01/55] [Synthetics UI] Remove 404 check (#147694) Fixes #147659 We were using a hook in a non-React context and that was messing up with how React kept state internally. That caused some other hooks to not save their own state and trigger infinite re-renders down the line in some scenarios. We will need to revisit how we check for 404 errors. Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../public/apps/synthetics/routes.tsx | 36 ++++--------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx index 76e61f784f69e..5f03369c5f382 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx @@ -16,7 +16,6 @@ import { APP_WRAPPER_CLASS } from '@kbn/core/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useInspectorContext } from '@kbn/observability-plugin/public'; import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-plugin/public'; -import { NotFoundPrompt } from '@kbn/shared-ux-prompt-not-found'; import { getSettingsRouteConfig } from './components/settings/route_config'; import { TestRunDetails } from './components/test_run_details/test_run_details'; import { ErrorDetailsPage } from './components/error_details/error_details_page'; @@ -59,14 +58,12 @@ import { MonitorSummary } from './components/monitor_details/monitor_summary/mon import { MonitorHistory } from './components/monitor_details/monitor_history/monitor_history'; import { MonitorErrors } from './components/monitor_details/monitor_errors/monitor_errors'; import { StepDetailPage } from './components/step_details_page/step_detail_page'; -import { useSelectedMonitor } from './components/monitor_details/hooks/use_selected_monitor'; export type RouteProps = LazyObservabilityPageTemplateProps & { path: string; component: React.FC; dataTestSubj: string; title: string; - is404?: () => boolean; }; const baseTitle = i18n.translate('xpack.synthetics.routes.baseTitle', { @@ -106,10 +103,6 @@ const getRoutes = ( values: { baseTitle }, }), path: MONITOR_ROUTE, - is404: function useIs404() { - const { error } = useSelectedMonitor(); - return error?.body.statusCode === 404; - }, component: () => ( @@ -124,10 +117,6 @@ const getRoutes = ( values: { baseTitle }, }), path: MONITOR_HISTORY_ROUTE, - is404: function useIs404() { - const { error } = useSelectedMonitor(); - return error?.body.statusCode === 404; - }, component: () => ( @@ -142,10 +131,6 @@ const getRoutes = ( values: { baseTitle }, }), path: MONITOR_ERRORS_ROUTE, - is404: function useIs404() { - const { error } = useSelectedMonitor(); - return error?.body.statusCode === 404; - }, component: () => ( @@ -428,25 +413,18 @@ export const PageRouter: FC = () => { component: RouteComponent, dataTestSubj, pageHeader, - is404, ...pageTemplateProps }: RouteProps) => (
- {is404?.() ? ( - - - - ) : ( - - - - )} + + +
) From bafe2132ba4199ba2638072ad9165c2419b0d119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 19 Dec 2022 10:34:43 +0000 Subject: [PATCH 02/55] [Table list view] Add state in URL (#145517) --- .../table_list/src/actions.ts | 22 +- .../table_list/src/components/table.tsx | 20 +- .../table_list/src/reducer.tsx | 30 +- .../table_list/src/services.tsx | 12 +- .../table_list/src/table_list_view.test.tsx | 9 +- .../table_list/src/table_list_view.tsx | 344 +++++++++++++++--- .../table_list/src/use_tags.ts | 6 +- .../table_list/src/use_url_state.ts | 53 +++ .../dashboard_listing.test.tsx.snap | 5 +- .../listing/dashboard_listing.test.tsx | 112 ++++-- .../listing/dashboard_listing.tsx | 2 +- src/plugins/dashboard/public/plugin.tsx | 16 +- .../saved_objects_tagging_oss/public/api.ts | 4 +- src/plugins/visualizations/public/plugin.ts | 9 +- .../dashboard/group4/dashboard_listing.ts | 4 +- .../public/services/tags/tags_cache.ts | 14 +- .../public/ui_api/parse_search_query.test.ts | 36 +- .../public/ui_api/parse_search_query.ts | 13 +- 18 files changed, 534 insertions(+), 177 deletions(-) create mode 100644 packages/content-management/table_list/src/use_url_state.ts diff --git a/packages/content-management/table_list/src/actions.ts b/packages/content-management/table_list/src/actions.ts index ba706025b036a..6ae2740381fcc 100644 --- a/packages/content-management/table_list/src/actions.ts +++ b/packages/content-management/table_list/src/actions.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ import type { IHttpFetchError } from '@kbn/core-http-browser'; -import type { CriteriaWithPagination, Direction, Query } from '@elastic/eui'; +import type { Query } from '@elastic/eui'; -import type { SortColumnField } from './components'; +import type { State, UserContentCommonSchema } from './table_list_view'; /** Action to trigger a fetch of the table items */ export interface OnFetchItemsAction { @@ -49,17 +49,14 @@ export interface OnSelectionChangeAction { } /** Action to update the state of the table whenever the sort or page size changes */ -export interface OnTableChangeAction { +export interface OnTableChangeAction { type: 'onTableChange'; - data: CriteriaWithPagination; -} - -/** Action to update the sort column of the table */ -export interface OnTableSortChangeAction { - type: 'onTableSortChange'; data: { - field: SortColumnField; - direction: Direction; + sort?: State['tableSort']; + page?: { + pageIndex: number; + pageSize: number; + }; }; } @@ -77,13 +74,12 @@ export interface OnSearchQueryChangeAction { }; } -export type Action = +export type Action = | OnFetchItemsAction | OnFetchItemsSuccessAction | OnFetchItemsErrorAction | DeleteItemsActions | OnSelectionChangeAction | OnTableChangeAction - | OnTableSortChangeAction | ShowConfirmDeleteItemsModalAction | OnSearchQueryChangeAction; diff --git a/packages/content-management/table_list/src/components/table.tsx b/packages/content-management/table_list/src/components/table.tsx index 1e4ee84204dd4..330eb67be4278 100644 --- a/packages/content-management/table_list/src/components/table.tsx +++ b/packages/content-management/table_list/src/components/table.tsx @@ -17,7 +17,6 @@ import { SearchFilterConfig, Direction, Query, - Ast, } from '@elastic/eui'; import { useServices } from '../services'; @@ -54,6 +53,7 @@ interface Props extends State, TagManageme deleteItems: TableListViewProps['deleteItems']; onSortChange: (column: SortColumnField, direction: Direction) => void; onTableChange: (criteria: CriteriaWithPagination) => void; + onTableSearchChange: (arg: { query: Query | null; queryText: string }) => void; clearTagSelection: () => void; } @@ -73,6 +73,7 @@ export function Table({ deleteItems, tableCaption, onTableChange, + onTableSearchChange, onSortChange, addOrRemoveExcludeTagFilter, addOrRemoveIncludeTagFilter, @@ -128,19 +129,6 @@ export function Table({ addOrRemoveIncludeTagFilter, }); - const onSearchQueryChange = useCallback( - (arg: { query: Query | null; queryText: string }) => { - dispatch({ - type: 'onSearchQueryChange', - data: { - query: arg.query ?? new Query(Ast.create([]), undefined, arg.queryText), - text: arg.queryText, - }, - }); - }, - [dispatch] - ); - const tableSortSelectFilter = useMemo(() => { return { type: 'custom_component', @@ -191,7 +179,7 @@ export function Table({ const search = useMemo(() => { return { - onChange: onSearchQueryChange, + onChange: onTableSearchChange, toolsLeft: renderToolsLeft(), query: searchQuery.query ?? undefined, box: { @@ -200,7 +188,7 @@ export function Table({ }, filters: searchFilters, }; - }, [onSearchQueryChange, renderToolsLeft, searchFilters, searchQuery.query]); + }, [onTableSearchChange, renderToolsLeft, searchFilters, searchQuery.query]); const noItemsMessage = ( () { + let sortColumnChanged = false; + return (state: State, action: Action): State => { switch (action.type) { case 'onFetchItems': { @@ -26,7 +28,10 @@ export function getReducer() { // We only get the state on the initial fetch of items // After that we don't want to reset the columns or change the sort after fetching hasUpdatedAtMetadata = Boolean(items.find((item) => Boolean(item.updatedAt))); - if (hasUpdatedAtMetadata) { + + // Only change the table sort if it hasn't been changed already. + // For example if its state comes from the URL, we don't want to override it here. + if (hasUpdatedAtMetadata && !sortColumnChanged) { tableSort = { field: 'updatedAt' as const, direction: 'desc' as const, @@ -58,6 +63,10 @@ export function getReducer() { }; } case 'onSearchQueryChange': { + if (action.data.text === state.searchQuery.text) { + return state; + } + return { ...state, searchQuery: action.data, @@ -65,23 +74,24 @@ export function getReducer() { }; } case 'onTableChange': { - const tableSort = (action.data.sort as State['tableSort']) ?? state.tableSort; + if (action.data.sort) { + sortColumnChanged = true; + } + + const tableSort = action.data.sort ?? state.tableSort; + const pageIndex = action.data.page?.pageIndex ?? state.pagination.pageIndex; + const pageSize = action.data.page?.pageSize ?? state.pagination.pageSize; + return { ...state, pagination: { ...state.pagination, - pageIndex: action.data.page.index, - pageSize: action.data.page.size, + pageIndex, + pageSize, }, tableSort, }; } - case 'onTableSortChange': { - return { - ...state, - tableSort: action.data, - }; - } case 'showConfirmDeleteItemsModal': { return { ...state, diff --git a/packages/content-management/table_list/src/services.tsx b/packages/content-management/table_list/src/services.tsx index a328641ad031f..5edc16d9a915d 100644 --- a/packages/content-management/table_list/src/services.tsx +++ b/packages/content-management/table_list/src/services.tsx @@ -47,11 +47,11 @@ export interface Services { notifyError: NotifyFn; currentAppId$: Observable; navigateToUrl: (url: string) => Promise | void; - searchQueryParser?: (searchQuery: string) => { + searchQueryParser?: (searchQuery: string) => Promise<{ searchQuery: string; references?: SavedObjectsFindOptionsReference[]; referencesToExclude?: SavedObjectsFindOptionsReference[]; - }; + }>; DateFormatterComp?: DateFormatter; /** Handler to retrieve the list of available tags */ getTagList: () => Tag[]; @@ -142,12 +142,12 @@ export interface TableListViewKibanaDependencies { useName?: boolean; tagField?: string; } - ) => { + ) => Promise<{ searchTerm: string; tagReferences: SavedObjectsFindOptionsReference[]; tagReferencesToExclude: SavedObjectsFindOptionsReference[]; valid: boolean; - }; + }>; getTagList: () => Tag[]; getTagIdsFromReferences: (references: SavedObjectsReference[]) => string[]; }; @@ -167,8 +167,8 @@ export const TableListViewKibanaProvider: FC = const searchQueryParser = useMemo(() => { if (savedObjectsTagging) { - return (searchQuery: string) => { - const res = savedObjectsTagging.ui.parseSearchQuery(searchQuery, { useName: true }); + return async (searchQuery: string) => { + const res = await savedObjectsTagging.ui.parseSearchQuery(searchQuery, { useName: true }); return { searchQuery: res.searchTerm, references: res.tagReferences, diff --git a/packages/content-management/table_list/src/table_list_view.test.tsx b/packages/content-management/table_list/src/table_list_view.test.tsx index 40893295fe340..92b03566cdc6f 100644 --- a/packages/content-management/table_list/src/table_list_view.test.tsx +++ b/packages/content-management/table_list/src/table_list_view.test.tsx @@ -49,6 +49,7 @@ const requiredProps: TableListViewProps = { tableListTitle: 'test title', findItems: jest.fn().mockResolvedValue({ total: 0, hits: [] }), getDetailViewLink: () => 'http://elastic.co', + urlStateEnabled: false, }; // FLAKY: https://github.com/elastic/kibana/issues/145267 @@ -66,7 +67,7 @@ describe.skip('TableListView', () => { WithServices(TableListView), { defaultProps: { ...requiredProps }, - memoryRouter: { wrapComponent: false }, + memoryRouter: { wrapComponent: true }, } ); @@ -333,7 +334,7 @@ describe.skip('TableListView', () => { WithServices(TableListView, { TagList: getTagList({ references: [] }) }), { defaultProps: { ...requiredProps }, - memoryRouter: { wrapComponent: false }, + memoryRouter: { wrapComponent: true }, } ); @@ -544,7 +545,7 @@ describe.skip('TableListView', () => { WithServices(TableListView), { defaultProps: { ...requiredProps }, - memoryRouter: { wrapComponent: false }, + memoryRouter: { wrapComponent: true }, } ); @@ -602,7 +603,7 @@ describe.skip('TableListView', () => { }), { defaultProps: { ...requiredProps }, - memoryRouter: { wrapComponent: false }, + memoryRouter: { wrapComponent: true }, } ); diff --git a/packages/content-management/table_list/src/table_list_view.tsx b/packages/content-management/table_list/src/table_list_view.tsx index 706c39bc7b266..9342a5076a38f 100644 --- a/packages/content-management/table_list/src/table_list_view.tsx +++ b/packages/content-management/table_list/src/table_list_view.tsx @@ -38,10 +38,10 @@ import { } from './components'; import { useServices } from './services'; import type { SavedObjectsReference, SavedObjectsFindOptionsReference } from './services'; -import type { Action } from './actions'; import { getReducer } from './reducer'; import type { SortColumnField } from './components'; import { useTags } from './use_tags'; +import { useUrlState } from './use_url_state'; interface ContentEditorConfig extends Pick { @@ -59,6 +59,7 @@ export interface Props; + urlStateEnabled?: boolean; /** * Id of the heading element describing the table. This id will be used as `aria-labelledby` of the wrapper element. * If the table is not empty, this component renders its own h1 element using the same id. @@ -131,7 +132,89 @@ export interface UserContentCommonSchema { }; } -const ast = Ast.create([]); +export interface URLState { + s?: string; + sort?: { + field: SortColumnField; + direction: Direction; + }; + [key: string]: unknown; +} + +interface URLQueryParams { + s?: string; + title?: string; + sort?: string; + sortdir?: string; + [key: string]: unknown; +} + +/** + * Deserializer to convert the URL query params to a sanitized object + * we can rely on in this component. + * + * @param params The URL query params + * @returns The URLState sanitized + */ +const urlStateDeserializer = (params: URLQueryParams): URLState => { + const stateFromURL: URLState = {}; + const sanitizedParams = { ...params }; + + // If we declare 2 or more query params with the same key in the URL + // we will receive an array of value back when parsed. It is probably + // a mistake from the user so we'll sanitize the data before continuing. + ['s', 'title', 'sort', 'sortdir'].forEach((key: string) => { + if (Array.isArray(sanitizedParams[key])) { + sanitizedParams[key] = (sanitizedParams[key] as string[])[0]; + } + }); + + // For backward compability with the Dashboard app we will support both "s" and "title" passed + // in the query params. We might want to stop supporting both in a future release (v9.0?) + stateFromURL.s = sanitizedParams.s ?? sanitizedParams.title; + + if (sanitizedParams.sort === 'title' || sanitizedParams.sort === 'updatedAt') { + const field = sanitizedParams.sort === 'title' ? 'attributes.title' : 'updatedAt'; + + stateFromURL.sort = { field, direction: 'asc' }; + + if (sanitizedParams.sortdir === 'desc' || sanitizedParams.sortdir === 'asc') { + stateFromURL.sort.direction = sanitizedParams.sortdir; + } + } + + return stateFromURL; +}; + +/** + * Serializer to convert the updated state of the component into query params in the URL + * + * @param updated The updated state of our component that we want to persist in the URL + * @returns The query params (flatten object) to update the URL + */ +const urlStateSerializer = (updated: { + s?: string; + sort?: { field: 'title' | 'updatedAt'; direction: Direction }; +}) => { + const updatedQueryParams: Partial = {}; + + if (updated.sort) { + updatedQueryParams.sort = updated.sort.field; + updatedQueryParams.sortdir = updated.sort.direction; + } + + if (updated.s !== undefined) { + updatedQueryParams.s = updated.s; + updatedQueryParams.title = undefined; + } + + if (typeof updatedQueryParams.s === 'string' && updatedQueryParams.s.trim() === '') { + updatedQueryParams.s = undefined; + updatedQueryParams.title = undefined; + } + + return updatedQueryParams; +}; function TableListViewComp({ tableListTitle, @@ -142,6 +225,7 @@ function TableListViewComp({ headingId, initialPageSize, listingLimit, + urlStateEnabled = true, customTableColumn, emptyPrompt, findItems, @@ -150,7 +234,7 @@ function TableListViewComp({ deleteItems, getDetailViewLink, onClickTitle, - id = 'userContent', + id: listingId = 'userContent', contentEditor = { enabled: false }, children, titleColumnName, @@ -185,36 +269,49 @@ function TableListViewComp({ searchQueryParser, notifyError, DateFormatterComp, + getTagList, } = useServices(); + const openContentEditor = useOpenContentEditor(); + + const [urlState, setUrlState] = useUrlState({ + queryParamsDeserializer: urlStateDeserializer, + queryParamsSerializer: urlStateSerializer, + }); + const reducer = useMemo(() => { return getReducer(); }, []); - const [state, dispatch] = useReducer<(state: State, action: Action) => State>(reducer, { - items: [], - totalItems: 0, - hasInitialFetchReturned: false, - isFetchingItems: false, - isDeletingItems: false, - showDeleteModal: false, - hasUpdatedAtMetadata: false, - selectedIds: [], - searchQuery: - initialQuery !== undefined - ? { text: initialQuery, query: new Query(ast, undefined, initialQuery) } - : { text: '', query: new Query(ast, undefined, '') }, - pagination: { - pageIndex: 0, - totalItemCount: 0, - pageSize: initialPageSize, - pageSizeOptions: uniq([10, 20, 50, initialPageSize]).sort(), - }, - tableSort: { - field: 'attributes.title' as const, - direction: 'asc', - }, - }); + const initialState = useMemo>( + () => ({ + items: [], + totalItems: 0, + hasInitialFetchReturned: false, + isFetchingItems: false, + isDeletingItems: false, + showDeleteModal: false, + hasUpdatedAtMetadata: false, + selectedIds: [], + searchQuery: + initialQuery !== undefined + ? { text: initialQuery, query: new Query(Ast.create([]), undefined, initialQuery) } + : { text: '', query: new Query(Ast.create([]), undefined, '') }, + pagination: { + pageIndex: 0, + totalItemCount: 0, + pageSize: initialPageSize, + pageSizeOptions: uniq([10, 20, 50, initialPageSize]).sort(), + }, + tableSort: { + field: 'attributes.title' as const, + direction: 'asc', + }, + }), + [initialPageSize, initialQuery] + ); + + const [state, dispatch] = useReducer(reducer, initialState); const { searchQuery, @@ -247,11 +344,9 @@ function TableListViewComp({ searchQuery: searchQueryParsed, references, referencesToExclude, - } = searchQueryParser?.(searchQuery.text) ?? { - searchQuery: searchQuery.text, - references: undefined, - referencesToExclude: undefined, - }; + } = searchQueryParser + ? await searchQueryParser(searchQuery.text) + : { searchQuery: searchQuery.text, references: undefined, referencesToExclude: undefined }; const response = await findItems(searchQueryParsed, { references, referencesToExclude }); @@ -275,14 +370,20 @@ function TableListViewComp({ } }, [searchQueryParser, findItems, searchQuery.text]); - const openContentEditor = useOpenContentEditor(); + const updateQuery = useCallback( + (query: Query) => { + if (urlStateEnabled) { + setUrlState({ s: query.text }); + return; + } - const updateQuery = useCallback((query: Query) => { - dispatch({ - type: 'onSearchQueryChange', - data: { query, text: query.text }, - }); - }, []); + dispatch({ + type: 'onSearchQueryChange', + data: { query, text: query.text }, + }); + }, + [urlStateEnabled, setUrlState] + ); const { addOrRemoveIncludeTagFilter, @@ -336,7 +437,7 @@ function TableListViewComp({ render: (field: keyof T, record: T) => { return ( - id={id} + id={listingId} item={record} getDetailViewLink={getDetailViewLink} onClickTitle={onClickTitle} @@ -439,7 +540,7 @@ function TableListViewComp({ customTableColumn, hasUpdatedAtMetadata, editItem, - id, + listingId, getDetailViewLink, onClickTitle, searchQuery.text, @@ -461,16 +562,79 @@ function TableListViewComp({ // ------------ // Callbacks // ------------ - const onSortChange = useCallback((field: SortColumnField, direction: Direction) => { - dispatch({ - type: 'onTableSortChange', - data: { field, direction }, - }); - }, []); + const onTableSearchChange = useCallback( + (arg: { query: Query | null; queryText: string }) => { + const query = arg.query ?? new Query(Ast.create([]), undefined, arg.queryText); + updateQuery(query); + }, + [updateQuery] + ); - const onTableChange = useCallback((criteria: CriteriaWithPagination) => { - dispatch({ type: 'onTableChange', data: criteria }); - }, []); + const updateTableSortAndPagination = useCallback( + (data: { + sort?: State['tableSort']; + page?: { + pageIndex: number; + pageSize: number; + }; + }) => { + if (data.sort && urlStateEnabled) { + setUrlState({ + sort: { + field: data.sort.field === 'attributes.title' ? 'title' : data.sort.field, + direction: data.sort.direction, + }, + }); + } + + if (data.page || !urlStateEnabled) { + dispatch({ + type: 'onTableChange', + data, + }); + } + }, + [setUrlState, urlStateEnabled] + ); + + const onSortChange = useCallback( + (field: SortColumnField, direction: Direction) => { + updateTableSortAndPagination({ + sort: { + field, + direction, + }, + }); + }, + [updateTableSortAndPagination] + ); + + const onTableChange = useCallback( + (criteria: CriteriaWithPagination) => { + const data: { + sort?: State['tableSort']; + page?: { + pageIndex: number; + pageSize: number; + }; + } = {}; + + if (criteria.sort) { + data.sort = { + field: criteria.sort.field as SortColumnField, + direction: criteria.sort.direction, + }; + } + + data.page = { + pageIndex: criteria.page.index, + pageSize: criteria.page.size, + }; + + updateTableSortAndPagination(data); + }, + [updateTableSortAndPagination] + ); const deleteSelectedItems = useCallback(async () => { if (isDeletingItems) { @@ -573,6 +737,85 @@ function TableListViewComp({ // ------------ useDebounce(fetchItems, 300, [fetchItems]); + useEffect(() => { + if (!urlStateEnabled) { + return; + } + + // Update our Query instance based on the URL "s" text + const updateQueryFromURL = async (text: string = '') => { + let ast = Ast.create([]); + let termMatch = text; + + if (searchQueryParser) { + // Parse possible tags in the search text + const { + references, + referencesToExclude, + searchQuery: searchTerm, + } = await searchQueryParser(text); + + termMatch = searchTerm; + + if (references?.length || referencesToExclude?.length) { + const allTags = getTagList(); + + if (references?.length) { + references.forEach(({ id: refId }) => { + const tag = allTags.find(({ id }) => id === refId); + if (tag) { + ast = ast.addOrFieldValue('tag', tag.name, true, 'eq'); + } + }); + } + + if (referencesToExclude?.length) { + referencesToExclude.forEach(({ id: refId }) => { + const tag = allTags.find(({ id }) => id === refId); + if (tag) { + ast = ast.addOrFieldValue('tag', tag.name, false, 'eq'); + } + }); + } + } + } + + if (termMatch.trim() !== '') { + ast = ast.addClause({ type: 'term', value: termMatch, match: 'must' }); + } + + const updatedQuery = new Query(ast, undefined, text); + + dispatch({ + type: 'onSearchQueryChange', + data: { + query: updatedQuery, + text, + }, + }); + }; + + // Update our State "sort" based on the URL "sort" and "sortdir" + const updateSortFromURL = (sort?: URLState['sort']) => { + if (!sort) { + return; + } + + dispatch({ + type: 'onTableChange', + data: { + sort: { + field: sort.field, + direction: sort.direction, + }, + }, + }); + }; + + updateQueryFromURL(urlState.s); + updateSortFromURL(urlState.sort); + }, [urlState, searchQueryParser, getTagList, urlStateEnabled]); + useEffect(() => { isMounted.current = true; @@ -650,6 +893,7 @@ function TableListViewComp({ deleteItems={deleteItems} tableCaption={tableListTitle} onTableChange={onTableChange} + onTableSearchChange={onTableSearchChange} onSortChange={onSortChange} addOrRemoveIncludeTagFilter={addOrRemoveIncludeTagFilter} addOrRemoveExcludeTagFilter={addOrRemoveExcludeTagFilter} diff --git a/packages/content-management/table_list/src/use_tags.ts b/packages/content-management/table_list/src/use_tags.ts index c72f550bc54b3..345a3484306ff 100644 --- a/packages/content-management/table_list/src/use_tags.ts +++ b/packages/content-management/table_list/src/use_tags.ts @@ -43,8 +43,8 @@ export function useTags({ const updateTagClauseGetter = useCallback( (queryUpdater: QueryUpdater) => - (tag: Tag, q?: Query, doUpdate: boolean = true) => { - const updatedQuery = queryUpdater(q !== undefined ? q : query, tag); + (tag: Tag, q: Query = query, doUpdate: boolean = true) => { + const updatedQuery = queryUpdater(q, tag); if (doUpdate) { updateQuery(updatedQuery); } @@ -128,7 +128,7 @@ export function useTags({ } if (hasTagInExclude(tag, q)) { - // Already selected, remove the filter + // Already excluded, remove the filter removeTagFromExcludeClause(tag, q); return; } diff --git a/packages/content-management/table_list/src/use_url_state.ts b/packages/content-management/table_list/src/use_url_state.ts new file mode 100644 index 0000000000000..2406ed597fbb9 --- /dev/null +++ b/packages/content-management/table_list/src/use_url_state.ts @@ -0,0 +1,53 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import queryString from 'query-string'; +import { useCallback, useMemo, useState, useEffect } from 'react'; +import { useLocation, useHistory } from 'react-router-dom'; + +function useQuery = {}>() { + const { search } = useLocation(); + return useMemo(() => queryString.parse(search) as T, [search]); +} + +export function useUrlState< + T extends Record = {}, + Q extends Record = {} +>({ + queryParamsDeserializer, + queryParamsSerializer, +}: { + queryParamsDeserializer: (params: Q) => T; + queryParamsSerializer: (params: Record) => Partial; +}): [T, (updated: Record) => void] { + const history = useHistory(); + const params = useQuery(); + const [urlState, setUrlState] = useState({} as T); + + const updateQuerParams = useCallback( + (updated: Record) => { + const updatedQuery = queryParamsSerializer(updated); + + const queryParams = { + ...params, + ...updatedQuery, + }; + + history.replace({ + search: `?${queryString.stringify(queryParams, { encode: false })}`, + }); + }, + [history, params, queryParamsSerializer] + ); + + useEffect(() => { + const updatedState = queryParamsDeserializer(params); + setUrlState(updatedState); + }, [params, queryParamsDeserializer]); + + return [urlState, updateQuerParams]; +} diff --git a/src/plugins/dashboard/public/dashboard_app/listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/dashboard_app/listing/__snapshots__/dashboard_listing.test.tsx.snap index b16dd6e178a7a..074697e980d1b 100644 --- a/src/plugins/dashboard/public/dashboard_app/listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/dashboard_app/listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -78,7 +78,7 @@ exports[`after fetch When given a title that matches multiple dashboards, filter getDetailViewLink={[Function]} headingId="dashboardListingHeading" id="dashboard" - initialFilter="\\"search by title\\"" + initialFilter="search by title" tableListTitle="Dashboards" > { + return { + useLocation: () => ({ + search: '', + }), + useHistory: () => ({ + push: () => undefined, + }), + }; +}); + function makeDefaultProps(): DashboardListingProps { return { redirectTo: jest.fn(), @@ -49,6 +60,11 @@ function mountWith({ props: incomingProps }: { props?: DashboardListingProps }) { ui: { ...savedObjectsTagging, + parseSearchQuery: async () => ({ + searchTerm: '', + tagReferences: [], + tagReferencesToExclude: [], + }), components: { TagList: () => null, }, @@ -69,12 +85,15 @@ function mountWith({ props: incomingProps }: { props?: DashboardListingProps }) describe('after fetch', () => { test('renders all table rows', async () => { - const { component } = mountWith({}); - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); + let component: ReactWrapper; + + await act(async () => { + ({ component } = mountWith({})); + }); + // Ensure the state changes are reflected - component.update(); - expect(component).toMatchSnapshot(); + component!.update(); + expect(component!).toMatchSnapshot(); }); test('renders call to action when no dashboards exist', async () => { @@ -85,12 +104,15 @@ describe('after fetch', () => { hits: [], }); - const { component } = mountWith({}); - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); + let component: ReactWrapper; + + await act(async () => { + ({ component } = mountWith({})); + }); + // Ensure the state changes are reflected - component.update(); - expect(component).toMatchSnapshot(); + component!.update(); + expect(component!).toMatchSnapshot(); }); test('renders call to action with continue when no dashboards exist but one is in progress', async () => { @@ -105,23 +127,30 @@ describe('after fetch', () => { hits: [], }); - const { component } = mountWith({}); - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); + let component: ReactWrapper; + + await act(async () => { + ({ component } = mountWith({})); + }); + // Ensure the state changes are reflected - component.update(); - expect(component).toMatchSnapshot(); + component!.update(); + expect(component!).toMatchSnapshot(); }); test('initialFilter', async () => { const props = makeDefaultProps(); props.initialFilter = 'testFilter'; - const { component } = mountWith({ props }); - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); + + let component: ReactWrapper; + + await act(async () => { + ({ component } = mountWith({})); + }); + // Ensure the state changes are reflected - component.update(); - expect(component).toMatchSnapshot(); + component!.update(); + expect(component!).toMatchSnapshot(); }); test('When given a title that matches multiple dashboards, filter on the title', async () => { @@ -131,12 +160,16 @@ describe('after fetch', () => { ( pluginServices.getServices().dashboardSavedObject.findDashboards.findByTitle as jest.Mock ).mockResolvedValue(undefined); - const { component } = mountWith({ props }); - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); + + let component: ReactWrapper; + + await act(async () => { + ({ component } = mountWith({ props })); + }); + // Ensure the state changes are reflected - component.update(); - expect(component).toMatchSnapshot(); + component!.update(); + expect(component!).toMatchSnapshot(); expect(props.redirectTo).not.toHaveBeenCalled(); }); @@ -147,11 +180,15 @@ describe('after fetch', () => { ( pluginServices.getServices().dashboardSavedObject.findDashboards.findByTitle as jest.Mock ).mockResolvedValue({ id: 'you_found_me' }); - const { component } = mountWith({ props }); - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); + + let component: ReactWrapper; + + await act(async () => { + ({ component } = mountWith({ props })); + }); + // Ensure the state changes are reflected - component.update(); + component!.update(); expect(props.redirectTo).toHaveBeenCalledWith({ destination: 'dashboard', id: 'you_found_me', @@ -162,11 +199,14 @@ describe('after fetch', () => { test('showWriteControls', async () => { pluginServices.getServices().dashboardCapabilities.showWriteControls = false; - const { component } = mountWith({}); - // Ensure all promises resolve - await new Promise((resolve) => process.nextTick(resolve)); + let component: ReactWrapper; + + await act(async () => { + ({ component } = mountWith({})); + }); + // Ensure the state changes are reflected - component.update(); - expect(component).toMatchSnapshot(); + component!.update(); + expect(component!).toMatchSnapshot(); }); }); diff --git a/src/plugins/dashboard/public/dashboard_app/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/dashboard_app/listing/dashboard_listing.tsx index f7de6f0e08452..ea2ecb151fb29 100644 --- a/src/plugins/dashboard/public/dashboard_app/listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/dashboard_app/listing/dashboard_listing.tsx @@ -145,7 +145,7 @@ export const DashboardListing = ({ const listingLimit = uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING); const initialPageSize = uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING); - const defaultFilter = title ? `"${title}"` : ''; + const defaultFilter = title ? `${title}` : ''; const createItem = useCallback(() => { if (!dashboardSessionStorage.dashboardHasUnsavedEdits()) { diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 00901b0849bbe..84689c8feecce 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -187,14 +187,16 @@ export class DashboardPlugin // Do not save SEARCH_SESSION_ID into nav link, because of possible edge cases // that could lead to session restoration failure. // see: https://github.com/elastic/kibana/issues/87149 - if (newNavLink.includes(SEARCH_SESSION_ID)) { - newNavLink = replaceUrlHashQuery(newNavLink, (query) => { - delete query[SEARCH_SESSION_ID]; - return query; - }); - } - return newNavLink; + // We also don't want to store the table list view state. + // The question is: what _do_ we want to save here? :) + const tableListUrlState = ['s', 'title', 'sort', 'sortdir']; + return replaceUrlHashQuery(newNavLink, (query) => { + [SEARCH_SESSION_ID, ...tableListUrlState].forEach((param) => { + delete query[param]; + }); + return query; + }); }, }); diff --git a/src/plugins/saved_objects_tagging_oss/public/api.ts b/src/plugins/saved_objects_tagging_oss/public/api.ts index d7ea41c225f20..54afed5d6203c 100644 --- a/src/plugins/saved_objects_tagging_oss/public/api.ts +++ b/src/plugins/saved_objects_tagging_oss/public/api.ts @@ -46,7 +46,7 @@ export interface ITagsCache { /** * Return an observable that will emit everytime the cache's state mutates. */ - getState$(): Observable; + getState$(params?: { waitForInitialization?: boolean }): Observable; } /** @@ -160,7 +160,7 @@ export interface SavedObjectsTaggingApiUi { * } * ``` */ - parseSearchQuery(query: string, options?: ParseSearchQueryOptions): ParsedSearchQuery; + parseSearchQuery(query: string, options?: ParseSearchQueryOptions): Promise; /** * Returns the object ids for the tag references from given references array diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 1e92e69086e4a..6628bd8a2208e 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -23,6 +23,7 @@ import { createStartServicesGetter, Storage, withNotifyOnErrors, + replaceUrlHashQuery, } from '@kbn/kibana-utils-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; @@ -216,7 +217,13 @@ export class VisualizationsPlugin if (this.isLinkedToOriginatingApp?.()) { return core.http.basePath.prepend(VisualizeConstants.VISUALIZE_BASE_PATH); } - return urlToSave; + const tableListUrlState = ['s', 'title', 'sort', 'sortdir']; + return replaceUrlHashQuery(urlToSave, (query) => { + tableListUrlState.forEach((param) => { + delete query[param]; + }); + return query; + }); }, }); this.stopUrlTracking = () => { diff --git a/test/functional/apps/dashboard/group4/dashboard_listing.ts b/test/functional/apps/dashboard/group4/dashboard_listing.ts index 4a9827ce02f91..73d979ad140f3 100644 --- a/test/functional/apps/dashboard/group4/dashboard_listing.ts +++ b/test/functional/apps/dashboard/group4/dashboard_listing.ts @@ -152,7 +152,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('preloads search filter bar when there is no match', async function () { const searchFilter = await listingTable.getSearchFilterValue(); - expect(searchFilter).to.equal('"nodashboardsnamedme"'); + expect(searchFilter).to.equal('nodashboardsnamedme'); }); it('stays on listing page if title matches two dashboards', async function () { @@ -172,7 +172,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('preloads search filter bar when there is more than one match', async function () { const searchFilter = await listingTable.getSearchFilterValue(); - expect(searchFilter).to.equal('"two words"'); + expect(searchFilter).to.equal('two words'); }); it('matches a title with many special characters', async function () { diff --git a/x-pack/plugins/saved_objects_tagging/public/services/tags/tags_cache.ts b/x-pack/plugins/saved_objects_tagging/public/services/tags/tags_cache.ts index 478218c3f3597..51539c970c7f3 100644 --- a/x-pack/plugins/saved_objects_tagging/public/services/tags/tags_cache.ts +++ b/x-pack/plugins/saved_objects_tagging/public/services/tags/tags_cache.ts @@ -7,7 +7,7 @@ import { Duration } from 'moment'; import { Observable, BehaviorSubject, Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { takeUntil, first, mergeMap } from 'rxjs/operators'; import { ITagsCache } from '@kbn/saved-objects-tagging-oss-plugin/public'; import { Tag, TagAttributes } from '../../../common/types'; @@ -41,6 +41,7 @@ export class TagsCache implements ITagsCache, ITagsChangeListener { private readonly internal$: BehaviorSubject; private readonly public$: Observable; private readonly stop$: Subject; + private isInitialized$: BehaviorSubject; constructor({ refreshHandler, refreshInterval }: TagsCacheOptions) { this.refreshHandler = refreshHandler; @@ -49,10 +50,12 @@ export class TagsCache implements ITagsCache, ITagsChangeListener { this.stop$ = new Subject(); this.internal$ = new BehaviorSubject([]); this.public$ = this.internal$.pipe(takeUntil(this.stop$)); + this.isInitialized$ = new BehaviorSubject(false); } public async initialize() { await this.refresh(); + this.isInitialized$.next(true); if (this.refreshInterval) { this.intervalId = window.setInterval(() => { @@ -74,8 +77,13 @@ export class TagsCache implements ITagsCache, ITagsChangeListener { return this.internal$.getValue(); } - public getState$() { - return this.public$; + public getState$({ waitForInitialization = false }: { waitForInitialization?: boolean } = {}) { + return waitForInitialization + ? this.isInitialized$.pipe( + first((isInitialized) => isInitialized), + mergeMap(() => this.public$) + ) + : this.public$; } public onDelete(id: string) { diff --git a/x-pack/plugins/saved_objects_tagging/public/ui_api/parse_search_query.test.ts b/x-pack/plugins/saved_objects_tagging/public/ui_api/parse_search_query.test.ts index 15e2349af47dc..63a1528224924 100644 --- a/x-pack/plugins/saved_objects_tagging/public/ui_api/parse_search_query.test.ts +++ b/x-pack/plugins/saved_objects_tagging/public/ui_api/parse_search_query.test.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { of } from 'rxjs'; import { SavedObjectsTaggingApiUi } from '@kbn/saved-objects-tagging-oss-plugin/public'; import { tagsCacheMock } from '../services/tags/tags_cache.mock'; import { createTag } from '../../common/test_utils'; @@ -27,15 +27,15 @@ describe('parseSearchQuery', () => { beforeEach(() => { cache = tagsCacheMock.create(); - cache.getState.mockReturnValue(tags); + cache.getState$.mockReturnValue(of(tags)); parseSearchQuery = buildParseSearchQuery({ cache }); }); - it('returns the search term when there is no field clause', () => { + it('returns the search term when there is no field clause', async () => { const searchTerm = 'my search term'; - expect(parseSearchQuery(searchTerm)).toEqual({ + expect(await parseSearchQuery(searchTerm)).toEqual({ searchTerm, tagReferences: [], tagReferencesToExclude: [], @@ -43,10 +43,10 @@ describe('parseSearchQuery', () => { }); }); - it('returns the raw search term when the syntax is not valid', () => { + it('returns the raw search term when the syntax is not valid', async () => { const searchTerm = 'tag:id-1 [search term]'; - expect(parseSearchQuery(searchTerm)).toEqual({ + expect(await parseSearchQuery(searchTerm)).toEqual({ searchTerm, tagReferences: [], tagReferencesToExclude: [], @@ -54,10 +54,10 @@ describe('parseSearchQuery', () => { }); }); - it('returns the tag references matching the tag field clause when using `useName: false`', () => { + it('returns the tag references matching the tag field clause when using `useName: false`', async () => { const searchTerm = 'tag:(id-1 OR id-2) my search term'; - expect(parseSearchQuery(searchTerm, { useName: false })).toEqual({ + expect(await parseSearchQuery(searchTerm, { useName: false })).toEqual({ searchTerm: 'my search term', tagReferences: [tagRef('id-1'), tagRef('id-2')], tagReferencesToExclude: [], @@ -65,10 +65,10 @@ describe('parseSearchQuery', () => { }); }); - it('returns the tag references to exclude matching the tag field clause when using `useName: false`', () => { + it('returns the tag references to exclude matching the tag field clause when using `useName: false`', async () => { const searchTerm = '-tag:(id-1 OR id-2) my search term'; - expect(parseSearchQuery(searchTerm, { useName: false })).toEqual({ + expect(await parseSearchQuery(searchTerm, { useName: false })).toEqual({ searchTerm: 'my search term', tagReferences: [], tagReferencesToExclude: [tagRef('id-1'), tagRef('id-2')], @@ -76,10 +76,10 @@ describe('parseSearchQuery', () => { }); }); - it('returns the tag references matching the tag field clause when using `useName: true`', () => { + it('returns the tag references matching the tag field clause when using `useName: true`', async () => { const searchTerm = 'tag:(name-1 OR name-2) my search term'; - expect(parseSearchQuery(searchTerm, { useName: true })).toEqual({ + expect(await parseSearchQuery(searchTerm, { useName: true })).toEqual({ searchTerm: 'my search term', tagReferences: [tagRef('id-1'), tagRef('id-2')], tagReferencesToExclude: [], @@ -87,10 +87,10 @@ describe('parseSearchQuery', () => { }); }); - it('returns the tag references to exclude matching the tag field clause when using `useName: true`', () => { + it('returns the tag references to exclude matching the tag field clause when using `useName: true`', async () => { const searchTerm = '-tag:(name-1 OR name-2) my search term'; - expect(parseSearchQuery(searchTerm, { useName: true })).toEqual({ + expect(await parseSearchQuery(searchTerm, { useName: true })).toEqual({ searchTerm: 'my search term', tagReferences: [], tagReferencesToExclude: [tagRef('id-1'), tagRef('id-2')], @@ -98,10 +98,10 @@ describe('parseSearchQuery', () => { }); }); - it('uses the `tagField` option', () => { + it('uses the `tagField` option', async () => { const searchTerm = 'custom:(name-1 OR name-2) my search term'; - expect(parseSearchQuery(searchTerm, { tagField: 'custom' })).toEqual({ + expect(await parseSearchQuery(searchTerm, { tagField: 'custom' })).toEqual({ searchTerm: 'my search term', tagReferences: [tagRef('id-1'), tagRef('id-2')], tagReferencesToExclude: [], @@ -109,10 +109,10 @@ describe('parseSearchQuery', () => { }); }); - it('ignores names not in the cache', () => { + it('ignores names not in the cache', async () => { const searchTerm = 'tag:(name-1 OR missing-name) my search term'; - expect(parseSearchQuery(searchTerm, { useName: true })).toEqual({ + expect(await parseSearchQuery(searchTerm, { useName: true })).toEqual({ searchTerm: 'my search term', tagReferences: [tagRef('id-1')], tagReferencesToExclude: [], diff --git a/x-pack/plugins/saved_objects_tagging/public/ui_api/parse_search_query.ts b/x-pack/plugins/saved_objects_tagging/public/ui_api/parse_search_query.ts index 8f22fcea3f782..eef74321cb9cb 100644 --- a/x-pack/plugins/saved_objects_tagging/public/ui_api/parse_search_query.ts +++ b/x-pack/plugins/saved_objects_tagging/public/ui_api/parse_search_query.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { lastValueFrom } from 'rxjs'; +import { first } from 'rxjs/operators'; import { Query } from '@elastic/eui'; import { SavedObjectsFindOptionsReference } from '@kbn/core/public'; import { @@ -20,7 +22,10 @@ export interface BuildParseSearchQueryOptions { export const buildParseSearchQuery = ({ cache, }: BuildParseSearchQueryOptions): SavedObjectsTaggingApiUi['parseSearchQuery'] => { - return (query: string, { tagField = 'tag', useName = true }: ParseSearchQueryOptions = {}) => { + return async ( + query: string, + { tagField = 'tag', useName = true }: ParseSearchQueryOptions = {} + ) => { let parsed: Query; try { @@ -73,11 +78,15 @@ export const buildParseSearchQuery = ({ { selectedTags: [], excludedTags: [] } as { selectedTags: string[]; excludedTags: string[] } ); + const tagsInCache = await lastValueFrom( + cache.getState$({ waitForInitialization: true }).pipe(first()) + ); + const tagsToReferences = (tagNames: string[]) => { if (useName) { const references: SavedObjectsFindOptionsReference[] = []; tagNames.forEach((tagName) => { - const found = cache.getState().find((tag) => tag.name === tagName); + const found = tagsInCache.find((tag) => tag.name === tagName); if (found) { references.push({ type: 'tag', From 5f21dbe618451b7ad280cf01e81c370c168fc97b Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Mon, 19 Dec 2022 11:33:21 +0000 Subject: [PATCH 03/55] Revert "Include client IP address in audit log" (#147747) Reverts elastic/kibana#147526 Reverting due to errors when using `FakeRequest`: ``` TypeError: Cannot read properties of undefined (reading 'remoteAddress') at KibanaSocket.get remoteAddress [as remoteAddress] (/Users/shahzad-16/elastic/kibana/node_modules/@kbn/core-http-router-server-internal/target_node/src/socket.js:25:24) at Object.log (/Users/shahzad-16/elastic/kibana/x-pack/plugins/security/server/audit/audit_service.ts:95:32) at runMicrotasks () at processTicksAndRejections (node:internal/process/task_queues:96:5) Terminating process... server crashed with status code 1 ``` --- docs/user/security/audit-logging.asciidoc | 8 -- .../src/socket.test.ts | 7 -- .../src/socket.ts | 4 - .../core-http-server/src/router/socket.ts | 7 -- .../security/server/audit/audit_events.ts | 109 +++++++----------- .../server/audit/audit_service.test.ts | 44 +------ .../security/server/audit/audit_service.ts | 27 ----- .../tests/audit/audit_log.ts | 6 - 8 files changed, 44 insertions(+), 168 deletions(-) diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index b83eea1dc5314..5f6fe746814e5 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -407,19 +407,11 @@ Example: `[marketing]` | *Field* | *Description* -| `client.ip` -| Client IP address. - | `http.request.method` | HTTP request method. Example: `get`, `post`, `put`, `delete` -| `http.request.headers.x-forwarded-for` -| `X-Forwarded-For` request header used to identify the originating client IP address when connecting through proxy servers. - -Example: `161.66.20.177, 236.198.214.101` - | `url.domain` | Domain of the URL. diff --git a/packages/core/http/core-http-router-server-internal/src/socket.test.ts b/packages/core/http/core-http-router-server-internal/src/socket.test.ts index b39c0e5514905..389c08825d51b 100644 --- a/packages/core/http/core-http-router-server-internal/src/socket.test.ts +++ b/packages/core/http/core-http-router-server-internal/src/socket.test.ts @@ -152,11 +152,4 @@ describe('KibanaSocket', () => { expect(socket.authorizationError).toBe(authorizationError); }); }); - - describe('remoteAddress', () => { - it('mirrors the value of net.Socket instance', () => { - const socket = new KibanaSocket({ remoteAddress: '1.1.1.1' } as Socket); - expect(socket.remoteAddress).toBe('1.1.1.1'); - }); - }); }); diff --git a/packages/core/http/core-http-router-server-internal/src/socket.ts b/packages/core/http/core-http-router-server-internal/src/socket.ts index 03e70624921b4..14923a51e9f7f 100644 --- a/packages/core/http/core-http-router-server-internal/src/socket.ts +++ b/packages/core/http/core-http-router-server-internal/src/socket.ts @@ -20,10 +20,6 @@ export class KibanaSocket implements IKibanaSocket { return this.socket instanceof TLSSocket ? this.socket.authorizationError : undefined; } - public get remoteAddress() { - return this.socket.remoteAddress; - } - constructor(private readonly socket: Socket) {} getPeerCertificate(detailed: true): DetailedPeerCertificate | null; diff --git a/packages/core/http/core-http-server/src/router/socket.ts b/packages/core/http/core-http-server/src/router/socket.ts index b4c53a95daab3..cc47373a583f2 100644 --- a/packages/core/http/core-http-server/src/router/socket.ts +++ b/packages/core/http/core-http-server/src/router/socket.ts @@ -51,11 +51,4 @@ export interface IKibanaSocket { * only when `authorized` is `false`. */ readonly authorizationError?: Error; - - /** - * The string representation of the remote IP address. For example,`'74.125.127.100'` or - * `'2001:4860:a005::68'`. Value may be `undefined` if the socket is destroyed (for example, if - * the client disconnected). - */ - readonly remoteAddress?: string; } diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts index 2ec67784b2ac6..5e38d5d53f224 100644 --- a/x-pack/plugins/security/server/audit/audit_events.ts +++ b/x-pack/plugins/security/server/audit/audit_events.ts @@ -14,71 +14,6 @@ import type { EcsEventOutcome, EcsEventType, KibanaRequest, LogMeta } from '@kbn import type { AuthenticationProvider } from '../../common/model'; import type { AuthenticationResult } from '../authentication/authentication_result'; -/** - * Audit kibana schema using ECS format - */ -export interface AuditKibana { - /** - * The ID of the space associated with this event. - */ - space_id?: string; - /** - * The ID of the user session associated with this event. Each login attempt - * results in a unique session id. - */ - session_id?: string; - /** - * Saved object that was created, changed, deleted or accessed as part of this event. - */ - saved_object?: { - type: string; - id: string; - }; - /** - * Name of authentication provider associated with a login event. - */ - authentication_provider?: string; - /** - * Type of authentication provider associated with a login event. - */ - authentication_type?: string; - /** - * Name of Elasticsearch realm that has authenticated the user. - */ - authentication_realm?: string; - /** - * Name of Elasticsearch realm where the user details were retrieved from. - */ - lookup_realm?: string; - /** - * Set of space IDs that a saved object was shared to. - */ - add_to_spaces?: readonly string[]; - /** - * Set of space IDs that a saved object was removed from. - */ - delete_from_spaces?: readonly string[]; -} - -type EcsHttp = Required['http']; -type EcsRequest = Required['request']; - -/** - * Audit request schema using ECS format - */ -export interface AuditRequest extends EcsRequest { - headers?: { - 'x-forwarded-for'?: string; - }; -} - -/** - * Audit http schema using ECS format - */ -export interface AuditHttp extends EcsHttp { - request?: AuditRequest; -} - /** * Audit event schema using ECS format: https://www.elastic.co/guide/en/ecs/1.12/index.html * @@ -89,8 +24,48 @@ export interface AuditHttp extends EcsHttp { */ export interface AuditEvent extends LogMeta { message: string; - kibana?: AuditKibana; - http?: AuditHttp; + kibana?: { + /** + * The ID of the space associated with this event. + */ + space_id?: string; + /** + * The ID of the user session associated with this event. Each login attempt + * results in a unique session id. + */ + session_id?: string; + /** + * Saved object that was created, changed, deleted or accessed as part of this event. + */ + saved_object?: { + type: string; + id: string; + }; + /** + * Name of authentication provider associated with a login event. + */ + authentication_provider?: string; + /** + * Type of authentication provider associated with a login event. + */ + authentication_type?: string; + /** + * Name of Elasticsearch realm that has authenticated the user. + */ + authentication_realm?: string; + /** + * Name of Elasticsearch realm where the user details were retrieved from. + */ + lookup_realm?: string; + /** + * Set of space IDs that a saved object was shared to. + */ + add_to_spaces?: readonly string[]; + /** + * Set of space IDs that a saved object was removed from. + */ + delete_from_spaces?: readonly string[]; + }; } export interface HttpRequestParams { diff --git a/x-pack/plugins/security/server/audit/audit_service.test.ts b/x-pack/plugins/security/server/audit/audit_service.test.ts index 4fae61b770912..900da83a6eb26 100644 --- a/x-pack/plugins/security/server/audit/audit_service.test.ts +++ b/x-pack/plugins/security/server/audit/audit_service.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type { Socket } from 'net'; import { lastValueFrom, Observable, of } from 'rxjs'; import { @@ -23,7 +22,6 @@ import { AuditService, createLoggingConfig, filterEvent, - getForwardedFor, RECORD_USAGE_INTERVAL, } from './audit_service'; @@ -188,26 +186,14 @@ describe('#asScoped', () => { recordAuditLoggingUsage, }); const request = httpServerMock.createKibanaRequest({ - socket: { remoteAddress: '3.3.3.3' } as Socket, - headers: { - 'x-forwarded-for': '1.1.1.1, 2.2.2.2', - }, kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' }, }); - await auditSetup.asScoped(request).log({ - message: 'MESSAGE', - event: { action: 'ACTION' }, - http: { request: { method: 'GET' } }, - }); - expect(logger.info).toHaveBeenLastCalledWith('MESSAGE', { + await auditSetup.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); + expect(logger.info).toHaveBeenCalledWith('MESSAGE', { event: { action: 'ACTION' }, kibana: { space_id: 'default', session_id: 'SESSION_ID' }, trace: { id: 'REQUEST_ID' }, - client: { ip: '3.3.3.3' }, - http: { - request: { method: 'GET', headers: { 'x-forwarded-for': '1.1.1.1, 2.2.2.2' } }, - }, user: { id: 'uid', name: 'jdoe', roles: ['admin'] }, }); audit.stop(); @@ -438,32 +424,6 @@ describe('#createLoggingConfig', () => { }); }); -describe('#getForwardedFor', () => { - it('extracts x-forwarded-for header from request', () => { - const request = httpServerMock.createKibanaRequest({ - headers: { - 'x-forwarded-for': '1.1.1.1', - }, - }); - expect(getForwardedFor(request)).toBe('1.1.1.1'); - }); - - it('concatenates multiple headers into single string in correct order', () => { - const request = httpServerMock.createKibanaRequest({ - headers: { - // @ts-expect-error Headers can be arrays but HAPI mocks are incorrectly typed - 'x-forwarded-for': ['1.1.1.1, 2.2.2.2', '3.3.3.3'], - }, - }); - expect(getForwardedFor(request)).toBe('1.1.1.1, 2.2.2.2, 3.3.3.3'); - }); - - it('returns undefined when header not present', () => { - const request = httpServerMock.createKibanaRequest(); - expect(getForwardedFor(request)).toBeUndefined(); - }); -}); - describe('#filterEvent', () => { let event: AuditEvent; diff --git a/x-pack/plugins/security/server/audit/audit_service.ts b/x-pack/plugins/security/server/audit/audit_service.ts index a163a75e71874..ff8a09df40198 100644 --- a/x-pack/plugins/security/server/audit/audit_service.ts +++ b/x-pack/plugins/security/server/audit/audit_service.ts @@ -162,8 +162,6 @@ export class AuditService { const spaceId = getSpaceId(request); const user = getCurrentUser(request); const sessionId = await getSID(request); - const forwardedFor = getForwardedFor(request); - log({ ...event, user: @@ -179,18 +177,6 @@ export class AuditService { ...event.kibana, }, trace: { id: request.id }, - client: { ip: request.socket.remoteAddress }, - http: forwardedFor - ? { - ...event.http, - request: { - ...event.http?.request, - headers: { - 'x-forwarded-for': forwardedFor, - }, - }, - } - : event.http, }); }, enabled, @@ -257,16 +243,3 @@ export function filterEvent( } return true; } - -/** - * Extracts `X-Forwarded-For` header(s) from `KibanaRequest`. - */ -export function getForwardedFor(request: KibanaRequest) { - const forwardedFor = request.headers['x-forwarded-for']; - - if (Array.isArray(forwardedFor)) { - return forwardedFor.join(', '); - } - - return forwardedFor; -} diff --git a/x-pack/test/security_api_integration/tests/audit/audit_log.ts b/x-pack/test/security_api_integration/tests/audit/audit_log.ts index df4dd45be0e10..553efa63431f1 100644 --- a/x-pack/test/security_api_integration/tests/audit/audit_log.ts +++ b/x-pack/test/security_api_integration/tests/audit/audit_log.ts @@ -55,7 +55,6 @@ export default function ({ getService }: FtrProviderContext) { await supertest .post('/internal/security/login') .set('kbn-xsrf', 'xxx') - .set('X-Forwarded-For', '1.1.1.1, 2.2.2.2') .send({ providerType: 'basic', providerName: 'basic', @@ -72,15 +71,12 @@ export default function ({ getService }: FtrProviderContext) { expect(loginEvent.event.outcome).to.be('success'); expect(loginEvent.trace.id).to.be.ok(); expect(loginEvent.user.name).to.be(username); - expect(loginEvent.client.ip).to.be.ok(); - expect(loginEvent.http.request.headers['x-forwarded-for']).to.be('1.1.1.1, 2.2.2.2'); }); it('logs audit events when failing to log in', async () => { await supertest .post('/internal/security/login') .set('kbn-xsrf', 'xxx') - .set('X-Forwarded-For', '1.1.1.1, 2.2.2.2') .send({ providerType: 'basic', providerName: 'basic', @@ -97,8 +93,6 @@ export default function ({ getService }: FtrProviderContext) { expect(loginEvent.event.outcome).to.be('failure'); expect(loginEvent.trace.id).to.be.ok(); expect(loginEvent.user).not.to.be.ok(); - expect(loginEvent.client.ip).to.be.ok(); - expect(loginEvent.http.request.headers['x-forwarded-for']).to.be('1.1.1.1, 2.2.2.2'); }); }); } From 136ed8014b54a0eea6faed515c2e3143e397e466 Mon Sep 17 00:00:00 2001 From: Joseph McElroy Date: Mon, 19 Dec 2022 11:36:08 +0000 Subject: [PATCH 04/55] [Enterprise Search] [Behavorial Analytics] Improve telemetry view Event for Collection View (#147681) Issue https://github.com/elastic/enterprise-search-team/issues/3322 This PR adds more information to the telemetry page view event for the collection view page to include the tab thats been selected. Clicking on each tab results in a page navigation so the implementation is pretty primitive. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../analytics_collection_view.test.tsx | 9 +++++++++ .../analytics_collection_view.tsx | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.test.tsx index 95e26ec8171d3..ebbd898f08641 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.test.tsx @@ -64,5 +64,14 @@ describe('AnalyticsOverview', () => { expect(wrapper.find(AnalyticsCollectionSettings)).toHaveLength(1); expect(mockActions.fetchAnalyticsCollection).toHaveBeenCalled(); }); + + it('sends correct telemetry page name for selected tab', async () => { + setMockValues(mockValues); + setMockActions(mockActions); + + const wrapper = shallow(); + + expect(wrapper.prop('pageViewTelemetry')).toBe('View Analytics Collection - settings'); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx index adc2b173e5b90..ae5d3e4166224 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx @@ -91,7 +91,7 @@ export const AnalyticsCollectionView: React.FC = () => { restrictWidth isLoading={isLoading} pageChrome={[...collectionViewBreadcrumbs]} - pageViewTelemetry="View Analytics Collection" + pageViewTelemetry={`View Analytics Collection - ${section}`} pageHeader={{ description: i18n.translate( 'xpack.enterpriseSearch.analytics.collectionsView.pageDescription', From 3c4ab973be94cd07cda983d3d4c8a22078364d17 Mon Sep 17 00:00:00 2001 From: Nodir Latipov Date: Mon, 19 Dec 2022 18:18:30 +0500 Subject: [PATCH 05/55] [Unified Search] Supports complex filters with AND/OR relationships (#143928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Describe the feature: Closes https://github.com/elastic/kibana/issues/144775 This PR allows users to create more than one filter at a time. It enhances the query builder by enabling users to create multiple filters simultaneously. It adds the capability to nest queries and use the logical OR operator in filter pills. image ## Tasks: - [x] Add the ability to add/edit multiple filters in one form: - [x] Replace the current implementation of adding and editing a filter with a filtersBuilder - `Vis-Editor`; - [x] Add combined filter support to Data plugin (mapAndFlattenFilters) - `App-Services`; - [x] Add the ability to update data in the Data plugin when updating values ​​in the filters builder - `App-Services`; - [x] Add hide `Edit as Query DSL` in popover case the filter inside FiltersBuilder is combinedFilter - `App-Services`; - [x] Update filter badge to display nested filters: - [x] Replace the current badge filter implementation with a new one - `Vis-Editor`; - [x] Clean up `FilterLabel` component after replace `FIlterBadge` component - `Vis-Editor`; - [x] When editing filters, those filters that belong to the same filter group should be edited - `Vis-Editor`; - [x] Update jest and functional tests with new functionality - `Vis-Editor`; - [x] Fix drag and drop behavior - `Vis-Editor`; Co-authored-by: Lukas Olson Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Alexey Antonov Co-authored-by: Stratoula Kalafateli Co-authored-by: Yaroslav Kuznietsov Co-authored-by: Andrea Del Rio --- packages/kbn-es-query/index.ts | 5 +- .../src/es_query/from_combined_filter.test.ts | 564 +++++++++++++++ .../src/es_query/from_combined_filter.ts | 49 ++ .../kbn-es-query/src/es_query/from_filters.ts | 8 +- ...ter.test.ts => from_nested_filter.test.ts} | 16 +- ...nested_filter.ts => from_nested_filter.ts} | 2 +- .../es_query/handle_combined_filter.test.ts | 610 ----------------- .../src/es_query/handle_combined_filter.ts | 41 -- packages/kbn-es-query/src/es_query/index.ts | 1 + .../src/es_query/migrate_filter.ts | 2 +- .../filters/build_filters/combined_filter.ts | 48 +- .../filters/helpers/compare_filters.test.ts | 74 +- .../src/filters/helpers/compare_filters.ts | 11 +- .../kbn-es-query/src/filters/helpers/index.ts | 1 + .../src/filters/helpers/update_filter.ts | 138 ++++ packages/kbn-es-query/src/filters/index.ts | 3 +- .../search/expressions/filters_to_ast.ts | 7 +- .../search/expressions/kibana_context.ts | 19 +- .../search/search_source/search_source.ts | 1 + .../filter_manager/lib/get_display_value.ts | 24 +- .../lib/get_index_pattern_from_filter.ts | 9 +- .../query/filter_manager/lib/map_filter.ts | 25 +- .../lib/mappers/map_combined.test.ts | 76 +++ .../lib/mappers/map_combined.ts | 24 + src/plugins/unified_search/jest.config.js | 1 + .../apply_filter_popover_content.tsx | 6 +- .../filter_badge/filter_badge.styles.ts | 30 + .../public/filter_badge/filter_badge.tsx | 84 +++ .../filter_badge_error_boundary.tsx | 43 ++ .../filter_badge/filter_badge_expression.tsx | 87 +++ .../filter_badge/filter_badge_group.tsx | 61 ++ .../filter_badge/filter_badge_invalid.tsx | 23 + .../filter_content.test.tsx.snap | 81 +++ .../filter_content/filter_content.test.tsx} | 26 +- .../filter_content/filter_content.tsx | 103 +++ .../filter_badge/filter_content/index.ts | 23 + .../public/filter_badge/i18n.ts | 16 + .../public/filter_badge/index.ts | 38 ++ .../filter_editor/filter_editor.styles.ts | 27 + .../filter_editor/filter_editor.test.tsx | 8 +- .../filter_editor/filter_editor.tsx | 643 ++++++++---------- .../filter_editor/generic_combo_box.styles.ts | 16 + .../filter_editor/generic_combo_box.tsx | 11 +- .../public/filter_bar/filter_editor/index.ts | 3 +- .../filter_editor/lib/filter_editor_utils.ts | 7 +- .../filter_editor/lib/filter_operators.ts | 67 +- .../filter_bar/filter_editor/lib/helpers.ts | 34 +- .../filter_editor/phrase_value_input.tsx | 69 +- .../filter_editor/phrases_values_input.tsx | 75 +- .../filter_editor/range_value_input.tsx | 10 +- .../filter_editor/truncated_label.test.tsx | 81 +++ .../filter_editor/truncated_label.tsx | 142 ++++ .../filter_editor/value_input_type.tsx | 8 +- .../filter_bar/filter_item/filter_item.scss | 2 +- .../filter_bar/filter_item/filter_item.tsx | 51 +- .../__snapshots__/filter_label.test.tsx.snap | 75 -- .../filter_bar/filter_label/filter_label.tsx | 97 --- .../public/filter_bar/filter_view/index.tsx | 26 +- .../public/filter_bar/index.tsx | 10 - .../filters_builder/__mock__/filters.ts | 90 +-- ...{filters_builder_context.ts => context.ts} | 3 +- .../filters_builder/filter_group.styles.ts | 30 + .../public/filters_builder/filter_group.tsx | 147 ++++ .../filter_item/actions/action_strings.ts | 40 ++ .../filter_item/actions/actions.tsx | 79 +++ .../filter_item/actions/index.tsx | 10 + .../filter_item/actions/minimised_actions.tsx | 38 ++ .../filter_item/actions/types.ts | 22 + .../filter_item/field_input.tsx | 111 +++ .../filter_item/filter_item.styles.ts | 49 ++ .../filter_item/filter_item.tsx | 320 +++++++++ .../index.ts} | 3 +- .../operator_input.tsx} | 24 +- .../filter_item/params_editor.tsx | 73 ++ .../params_editor_input.tsx} | 110 +-- .../filters_builder/filter_item/tooltip.tsx | 27 + .../filters_builder/filters_builder.styles.ts | 23 + .../filters_builder/filters_builder.tsx | 92 +-- .../filters_builder_filter_group.tsx | 149 ---- .../filters_builder_filter_item.tsx | 317 --------- ...ilters_builder_filter_item_field_input.tsx | 51 -- .../filters_builder/filters_builder_utils.ts | 360 ---------- ...{filters_builder_reducer.ts => reducer.ts} | 55 +- .../public/filters_builder/types.ts | 16 + .../filters_builder.test.ts} | 136 ++-- .../filters_builder/utils/filters_builder.ts | 194 ++++++ .../index.ts | 2 +- src/plugins/unified_search/public/index.ts | 3 +- .../add_filter_popover.styles.ts | 20 + .../query_string_input/add_filter_popover.tsx | 25 +- .../query_string_input/language_switcher.tsx | 13 +- .../query_string_input/no_data_popover.tsx | 37 +- .../query_string_input/query_bar_menu.tsx | 21 +- .../query_bar_menu_panels.tsx | 201 +++--- .../query_string_input/query_bar_top_row.tsx | 33 +- .../query_string_input/query_string_input.tsx | 68 +- .../public/utils/combined_filter.ts | 19 +- .../unified_search/public/utils/index.ts | 2 +- test/accessibility/apps/filter_panel.ts | 11 +- .../apps/context/_context_navigation.ts | 2 +- .../apps/context/_discover_navigation.ts | 2 +- test/functional/apps/context/_filters.ts | 82 ++- .../group1/dashboard_unsaved_state.ts | 2 +- .../dashboard/group2/dashboard_filtering.ts | 2 +- .../apps/dashboard/group2/full_screen_mode.ts | 2 +- .../group5/dashboard_error_handling.ts | 2 +- .../group5/saved_search_embeddable.ts | 2 +- .../functional/apps/dashboard/group5/share.ts | 2 +- .../controls/options_list.ts | 10 +- .../ccs_compatibility/_saved_queries.ts | 2 +- .../apps/discover/group1/_filter_editor.ts | 18 +- .../apps/discover/group1/_sidebar.ts | 4 +- .../apps/discover/group2/_adhoc_data_views.ts | 14 +- .../apps/visualize/group1/_data_table.ts | 6 +- .../group1/_data_table_nontimeindex.ts | 6 +- .../apps/visualize/group1/_embedding_chart.ts | 6 +- .../group3/_linked_saved_searches.ts | 8 +- .../apps/visualize/group6/_vega_chart.ts | 8 +- test/functional/services/filter_bar.ts | 267 ++++++-- .../test_suites/data_plugin/session.ts | 2 +- .../filter_value_label/filter_value_label.tsx | 16 +- .../investigate_in_timeline.cy.ts | 1 + .../cypress/screens/search_bar.ts | 2 +- .../cypress/screens/timeline.ts | 7 +- .../rules/description_step/helpers.test.tsx | 23 +- .../rules/description_step/helpers.tsx | 9 +- .../translations/translations/fr-FR.json | 14 +- .../translations/translations/ja-JP.json | 14 +- .../translations/translations/zh-CN.json | 14 +- .../pages/findings.ts | 2 +- .../apps/dashboard/group2/sync_colors.ts | 2 +- .../group3/reporting/download_csv.ts | 4 +- .../functional/apps/discover/reporting.ts | 2 +- .../apps/discover/saved_searches.ts | 2 +- .../apps/discover/value_suggestions.ts | 2 +- .../apps/discover/visualize_field.ts | 6 +- .../apps/lens/group1/fields_list.ts | 4 +- .../apps/lens/group1/persistent_context.ts | 2 +- .../functional/apps/lens/group2/dashboard.ts | 6 +- .../group2/show_underlying_data_dashboard.ts | 12 +- .../apps/lens/open_in_lens/tsvb/timeseries.ts | 2 +- .../apps/lens/open_in_lens/tsvb/top_n.ts | 2 +- .../apps/maps/group2/embeddable/dashboard.js | 19 +- .../apps/maps/group4/lens/choropleth_chart.ts | 6 +- .../index_data_visualizer_filters.ts | 12 +- .../apps/discover/search_source_alert.ts | 4 +- .../apps/ccs/ccs_discover.js | 2 +- 147 files changed, 4643 insertions(+), 2863 deletions(-) create mode 100644 packages/kbn-es-query/src/es_query/from_combined_filter.test.ts create mode 100644 packages/kbn-es-query/src/es_query/from_combined_filter.ts rename packages/kbn-es-query/src/es_query/{handle_nested_filter.test.ts => from_nested_filter.test.ts} (85%) rename packages/kbn-es-query/src/es_query/{handle_nested_filter.ts => from_nested_filter.ts} (97%) delete mode 100644 packages/kbn-es-query/src/es_query/handle_combined_filter.test.ts delete mode 100644 packages/kbn-es-query/src/es_query/handle_combined_filter.ts create mode 100644 packages/kbn-es-query/src/filters/helpers/update_filter.ts create mode 100644 src/plugins/data/public/query/filter_manager/lib/mappers/map_combined.test.ts create mode 100644 src/plugins/data/public/query/filter_manager/lib/mappers/map_combined.ts create mode 100644 src/plugins/unified_search/public/filter_badge/filter_badge.styles.ts create mode 100644 src/plugins/unified_search/public/filter_badge/filter_badge.tsx create mode 100644 src/plugins/unified_search/public/filter_badge/filter_badge_error_boundary.tsx create mode 100644 src/plugins/unified_search/public/filter_badge/filter_badge_expression.tsx create mode 100644 src/plugins/unified_search/public/filter_badge/filter_badge_group.tsx create mode 100644 src/plugins/unified_search/public/filter_badge/filter_badge_invalid.tsx create mode 100644 src/plugins/unified_search/public/filter_badge/filter_content/__snapshots__/filter_content.test.tsx.snap rename src/plugins/unified_search/public/{filter_bar/filter_label/filter_label.test.tsx => filter_badge/filter_content/filter_content.test.tsx} (66%) create mode 100644 src/plugins/unified_search/public/filter_badge/filter_content/filter_content.tsx create mode 100644 src/plugins/unified_search/public/filter_badge/filter_content/index.ts create mode 100644 src/plugins/unified_search/public/filter_badge/i18n.ts create mode 100644 src/plugins/unified_search/public/filter_badge/index.ts create mode 100644 src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.styles.ts create mode 100644 src/plugins/unified_search/public/filter_bar/filter_editor/generic_combo_box.styles.ts create mode 100644 src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.test.tsx create mode 100644 src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.tsx delete mode 100644 src/plugins/unified_search/public/filter_bar/filter_label/__snapshots__/filter_label.test.tsx.snap delete mode 100644 src/plugins/unified_search/public/filter_bar/filter_label/filter_label.tsx rename src/plugins/unified_search/public/filters_builder/{filters_builder_context.ts => context.ts} (91%) create mode 100644 src/plugins/unified_search/public/filters_builder/filter_group.styles.ts create mode 100644 src/plugins/unified_search/public/filters_builder/filter_group.tsx create mode 100644 src/plugins/unified_search/public/filters_builder/filter_item/actions/action_strings.ts create mode 100644 src/plugins/unified_search/public/filters_builder/filter_item/actions/actions.tsx create mode 100644 src/plugins/unified_search/public/filters_builder/filter_item/actions/index.tsx create mode 100644 src/plugins/unified_search/public/filters_builder/filter_item/actions/minimised_actions.tsx create mode 100644 src/plugins/unified_search/public/filters_builder/filter_item/actions/types.ts create mode 100644 src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx create mode 100644 src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts create mode 100644 src/plugins/unified_search/public/filters_builder/filter_item/filter_item.tsx rename src/plugins/unified_search/public/filters_builder/{filters_builder_types.ts => filter_item/index.ts} (88%) rename src/plugins/unified_search/public/filters_builder/{filters_builder_filter_item/filters_builder_filter_item_operator_input.tsx => filter_item/operator_input.tsx} (74%) create mode 100644 src/plugins/unified_search/public/filters_builder/filter_item/params_editor.tsx rename src/plugins/unified_search/public/filters_builder/{filters_builder_filter_item/filters_builder_filter_item_params_editor.tsx => filter_item/params_editor_input.tsx} (54%) create mode 100644 src/plugins/unified_search/public/filters_builder/filter_item/tooltip.tsx create mode 100644 src/plugins/unified_search/public/filters_builder/filters_builder.styles.ts delete mode 100644 src/plugins/unified_search/public/filters_builder/filters_builder_filter_group.tsx delete mode 100644 src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item.tsx delete mode 100644 src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_field_input.tsx delete mode 100644 src/plugins/unified_search/public/filters_builder/filters_builder_utils.ts rename src/plugins/unified_search/public/filters_builder/{filters_builder_reducer.ts => reducer.ts} (62%) create mode 100644 src/plugins/unified_search/public/filters_builder/types.ts rename src/plugins/unified_search/public/filters_builder/{filters_builder_utils.test.ts => utils/filters_builder.test.ts} (68%) create mode 100644 src/plugins/unified_search/public/filters_builder/utils/filters_builder.ts rename src/plugins/unified_search/public/filters_builder/{filters_builder_filter_item => utils}/index.ts (85%) create mode 100644 src/plugins/unified_search/public/query_string_input/add_filter_popover.styles.ts diff --git a/packages/kbn-es-query/index.ts b/packages/kbn-es-query/index.ts index 4ea965ea4b7a8..a2b529d005ea0 100644 --- a/packages/kbn-es-query/index.ts +++ b/packages/kbn-es-query/index.ts @@ -22,7 +22,6 @@ export type { ExistsFilter, FieldFilter, Filter, - FilterItem, FilterCompareOptions, FilterMeta, LatLon, @@ -38,6 +37,7 @@ export type { ScriptedPhraseFilter, ScriptedRangeFilter, TimeRange, + CombinedFilter, } from './src/filters'; export type { @@ -54,6 +54,7 @@ export { decorateQuery, luceneStringToDsl, migrateFilter, + fromCombinedFilter, isOfQueryType, isOfAggregateQueryType, getAggregateQueryMode, @@ -105,9 +106,11 @@ export { toggleFilterPinned, uniqFilters, unpinFilter, + updateFilter, extractTimeFilter, extractTimeRange, convertRangeFilterToTimeRange, + BooleanRelation, } from './src/filters'; export { diff --git a/packages/kbn-es-query/src/es_query/from_combined_filter.test.ts b/packages/kbn-es-query/src/es_query/from_combined_filter.test.ts new file mode 100644 index 0000000000000..8f80c0fdc58c8 --- /dev/null +++ b/packages/kbn-es-query/src/es_query/from_combined_filter.test.ts @@ -0,0 +1,564 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { fields } from '../filters/stubs'; +import { DataViewBase } from './types'; +import { fromCombinedFilter } from './from_combined_filter'; +import { + BooleanRelation, + buildCombinedFilter, + buildExistsFilter, + buildPhraseFilter, + buildPhrasesFilter, + buildRangeFilter, +} from '../filters'; + +describe('#fromCombinedFilter', function () { + const indexPattern: DataViewBase = { + id: 'logstash-*', + fields, + title: 'dataView', + }; + + const getField = (fieldName: string) => { + const field = fields.find(({ name }) => fieldName === name); + if (!field) throw new Error(`field ${name} does not exist`); + return field; + }; + + describe('AND relation', () => { + it('Generates an empty bool should clause with no filters', () => { + const filter = buildCombinedFilter(BooleanRelation.AND, [], indexPattern); + const result = fromCombinedFilter(filter); + expect(result.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + it('Generates a bool should clause with its sub-filters', () => { + const filters = [ + buildPhraseFilter(getField('extension'), 'value', indexPattern), + buildRangeFilter(getField('bytes'), { gte: 10 }, indexPattern), + buildExistsFilter(getField('machine.os'), indexPattern), + ]; + const filter = buildCombinedFilter(BooleanRelation.AND, filters, indexPattern); + const result = fromCombinedFilter(filter); + expect(result.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "match_phrase": Object { + "extension": "value", + }, + }, + Object { + "range": Object { + "bytes": Object { + "gte": 10, + }, + }, + }, + Object { + "exists": Object { + "field": "machine.os", + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + it('Handles negated sub-filters', () => { + const negatedFilter = buildPhrasesFilter(getField('extension'), ['tar', 'gz'], indexPattern); + negatedFilter.meta.negate = true; + const filters = [ + negatedFilter, + buildRangeFilter(getField('bytes'), { gte: 10 }, indexPattern), + buildExistsFilter(getField('machine.os'), indexPattern), + ]; + const filter = buildCombinedFilter(BooleanRelation.AND, filters, indexPattern); + const result = fromCombinedFilter(filter); + expect(result.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "bytes": Object { + "gte": 10, + }, + }, + }, + Object { + "exists": Object { + "field": "machine.os", + }, + }, + ], + "must": Array [], + "must_not": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "extension": "tar", + }, + }, + Object { + "match_phrase": Object { + "extension": "gz", + }, + }, + ], + }, + }, + ], + "should": Array [], + }, + } + `); + }); + + it('Preserves filter properties', () => { + const filters = [ + buildPhraseFilter(getField('extension'), 'value', indexPattern), + buildRangeFilter(getField('bytes'), { gte: 10 }, indexPattern), + buildExistsFilter(getField('machine.os'), indexPattern), + ]; + const filter = buildCombinedFilter(BooleanRelation.AND, filters, indexPattern); + const { query, ...rest } = fromCombinedFilter(filter); + expect(rest).toMatchInlineSnapshot(` + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": undefined, + "disabled": false, + "index": "logstash-*", + "negate": false, + "params": Array [ + Object { + "meta": Object { + "index": "logstash-*", + }, + "query": Object { + "match_phrase": Object { + "extension": "value", + }, + }, + }, + Object { + "meta": Object { + "field": "bytes", + "index": "logstash-*", + "params": Object {}, + }, + "query": Object { + "range": Object { + "bytes": Object { + "gte": 10, + }, + }, + }, + }, + Object { + "meta": Object { + "index": "logstash-*", + }, + "query": Object { + "exists": Object { + "field": "machine.os", + }, + }, + }, + ], + "relation": "AND", + "type": "combined", + }, + } + `); + }); + }); + + describe('OR relation', () => { + it('Generates an empty bool should clause with no filters', () => { + const filter = buildCombinedFilter(BooleanRelation.OR, [], indexPattern); + const result = fromCombinedFilter(filter); + expect(result.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [], + }, + } + `); + }); + + it('Generates a bool should clause with its sub-filters', () => { + const filters = [ + buildPhraseFilter(getField('extension'), 'value', indexPattern), + buildRangeFilter(getField('bytes'), { gte: 10 }, indexPattern), + buildExistsFilter(getField('machine.os'), indexPattern), + ]; + const filter = buildCombinedFilter(BooleanRelation.OR, filters, indexPattern); + const result = fromCombinedFilter(filter); + expect(result.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "filter": Array [ + Object { + "match_phrase": Object { + "extension": "value", + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "bytes": Object { + "gte": 10, + }, + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "exists": Object { + "field": "machine.os", + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + ], + }, + } + `); + }); + + it('Handles negated sub-filters', () => { + const negatedFilter = buildPhrasesFilter(getField('extension'), ['tar', 'gz'], indexPattern); + negatedFilter.meta.negate = true; + const filters = [ + negatedFilter, + buildRangeFilter(getField('bytes'), { gte: 10 }, indexPattern), + buildExistsFilter(getField('machine.os'), indexPattern), + ]; + const filter = buildCombinedFilter(BooleanRelation.OR, filters, indexPattern); + const result = fromCombinedFilter(filter); + expect(result.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "filter": Array [], + "must": Array [], + "must_not": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "extension": "tar", + }, + }, + Object { + "match_phrase": Object { + "extension": "gz", + }, + }, + ], + }, + }, + ], + "should": Array [], + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "bytes": Object { + "gte": 10, + }, + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "exists": Object { + "field": "machine.os", + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + ], + }, + } + `); + }); + + it('Preserves filter properties', () => { + const filters = [ + buildPhraseFilter(getField('extension'), 'value', indexPattern), + buildRangeFilter(getField('bytes'), { gte: 10 }, indexPattern), + buildExistsFilter(getField('machine.os'), indexPattern), + ]; + const filter = buildCombinedFilter(BooleanRelation.OR, filters, indexPattern); + const { query, ...rest } = fromCombinedFilter(filter); + expect(rest).toMatchInlineSnapshot(` + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": undefined, + "disabled": false, + "index": "logstash-*", + "negate": false, + "params": Array [ + Object { + "meta": Object { + "index": "logstash-*", + }, + "query": Object { + "match_phrase": Object { + "extension": "value", + }, + }, + }, + Object { + "meta": Object { + "field": "bytes", + "index": "logstash-*", + "params": Object {}, + }, + "query": Object { + "range": Object { + "bytes": Object { + "gte": 10, + }, + }, + }, + }, + Object { + "meta": Object { + "index": "logstash-*", + }, + "query": Object { + "exists": Object { + "field": "machine.os", + }, + }, + }, + ], + "relation": "OR", + "type": "combined", + }, + } + `); + }); + }); + + describe('Nested relations', () => { + it('Handles complex-nested filters with ANDs and ORs', () => { + const filters = [ + buildCombinedFilter( + BooleanRelation.OR, + [ + buildPhrasesFilter(getField('extension'), ['tar', 'gz'], indexPattern), + buildPhraseFilter(getField('ssl'), false, indexPattern), + buildCombinedFilter( + BooleanRelation.AND, + [ + buildPhraseFilter(getField('extension'), 'value', indexPattern), + buildRangeFilter(getField('bytes'), { gte: 10 }, indexPattern), + ], + indexPattern + ), + buildExistsFilter(getField('machine.os'), indexPattern), + ], + indexPattern + ), + buildPhrasesFilter(getField('machine.os.keyword'), ['foo', 'bar'], indexPattern), + ]; + const filter = buildCombinedFilter(BooleanRelation.AND, filters, indexPattern); + const result = fromCombinedFilter(filter); + expect(result.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "extension": "tar", + }, + }, + Object { + "match_phrase": Object { + "extension": "gz", + }, + }, + ], + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "match_phrase": Object { + "ssl": false, + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "filter": Array [ + Object { + "match_phrase": Object { + "extension": "value", + }, + }, + Object { + "range": Object { + "bytes": Object { + "gte": 10, + }, + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "exists": Object { + "field": "machine.os", + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "machine.os.keyword": "foo", + }, + }, + Object { + "match_phrase": Object { + "machine.os.keyword": "bar", + }, + }, + ], + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + }); +}); diff --git a/packages/kbn-es-query/src/es_query/from_combined_filter.ts b/packages/kbn-es-query/src/es_query/from_combined_filter.ts new file mode 100644 index 0000000000000..cb84bf8c54a89 --- /dev/null +++ b/packages/kbn-es-query/src/es_query/from_combined_filter.ts @@ -0,0 +1,49 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Filter, isCombinedFilter } from '../filters'; +import { DataViewBase } from './types'; +import { buildQueryFromFilters, EsQueryFiltersConfig } from './from_filters'; +import { BooleanRelation, CombinedFilter } from '../filters/build_filters'; + +const fromAndFilter = ( + filter: CombinedFilter, + dataViews?: DataViewBase | DataViewBase[], + options: EsQueryFiltersConfig = {} +) => { + const bool = buildQueryFromFilters(filter.meta.params, dataViews, options); + return { ...filter, query: { bool } }; +}; + +const fromOrFilter = ( + filter: CombinedFilter, + dataViews?: DataViewBase | DataViewBase[], + options: EsQueryFiltersConfig = {} +) => { + const should = filter.meta.params.map((subFilter) => ({ + bool: buildQueryFromFilters([subFilter], dataViews, options), + })); + const bool = { should, minimum_should_match: 1 }; + return { ...filter, query: { bool } }; +}; + +export const fromCombinedFilter = ( + filter: Filter, + dataViews?: DataViewBase | DataViewBase[], + options: EsQueryFiltersConfig = {} +): Filter => { + if (!isCombinedFilter(filter)) { + return filter; + } + + if (filter.meta.relation === BooleanRelation.AND) { + return fromAndFilter(filter, dataViews, options); + } + + return fromOrFilter(filter, dataViews, options); +}; diff --git a/packages/kbn-es-query/src/es_query/from_filters.ts b/packages/kbn-es-query/src/es_query/from_filters.ts index 9e2599cc6c70b..029dcf3e15dd8 100644 --- a/packages/kbn-es-query/src/es_query/from_filters.ts +++ b/packages/kbn-es-query/src/es_query/from_filters.ts @@ -12,8 +12,8 @@ import { migrateFilter } from './migrate_filter'; import { filterMatchesIndex } from './filter_matches_index'; import { Filter, cleanFilter, isFilterDisabled } from '../filters'; import { BoolQuery, DataViewBase } from './types'; -import { handleNestedFilter } from './handle_nested_filter'; -import { handleCombinedFilter } from './handle_combined_filter'; +import { fromNestedFilter } from './from_nested_filter'; +import { fromCombinedFilter } from './from_combined_filter'; /** * Create a filter that can be reversed for filters with negate set @@ -90,11 +90,11 @@ export const buildQueryFromFilters = ( .map((filter) => { const indexPattern = findIndexPattern(filter.meta?.index); const migratedFilter = migrateFilter(filter, indexPattern); - return handleNestedFilter(migratedFilter, indexPattern, { + return fromNestedFilter(migratedFilter, indexPattern, { ignoreUnmapped: nestedIgnoreUnmapped, }); }) - .map((filter) => handleCombinedFilter(filter, inputDataViews, options)) + .map((filter) => fromCombinedFilter(filter, inputDataViews, options)) .map(cleanFilter) .map(translateToQuery); }; diff --git a/packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts b/packages/kbn-es-query/src/es_query/from_nested_filter.test.ts similarity index 85% rename from packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts rename to packages/kbn-es-query/src/es_query/from_nested_filter.test.ts index 01ce78dfbab9f..624220f4c1a6c 100644 --- a/packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts +++ b/packages/kbn-es-query/src/es_query/from_nested_filter.test.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import { handleNestedFilter } from './handle_nested_filter'; +import { fromNestedFilter } from './from_nested_filter'; import { fields } from '../filters/stubs'; import { buildPhraseFilter, buildQueryFilter } from '../filters'; import { DataViewBase } from './types'; -describe('handleNestedFilter', function () { +describe('fromNestedFilter', function () { const indexPattern: DataViewBase = { id: 'logstash-*', fields, @@ -21,7 +21,7 @@ describe('handleNestedFilter', function () { it("should return the filter's query wrapped in nested query if the target field is nested", () => { const field = getField('nestedField.child'); const filter = buildPhraseFilter(field!, 'foo', indexPattern); - const result = handleNestedFilter(filter, indexPattern); + const result = fromNestedFilter(filter, indexPattern); expect(result).toEqual({ meta: { index: 'logstash-*', @@ -42,7 +42,7 @@ describe('handleNestedFilter', function () { it('should allow to configure ignore_unmapped', () => { const field = getField('nestedField.child'); const filter = buildPhraseFilter(field!, 'foo', indexPattern); - const result = handleNestedFilter(filter, indexPattern, { ignoreUnmapped: true }); + const result = fromNestedFilter(filter, indexPattern, { ignoreUnmapped: true }); expect(result).toEqual({ meta: { index: 'logstash-*', @@ -64,7 +64,7 @@ describe('handleNestedFilter', function () { it('should return filter untouched if it does not target a nested field', () => { const field = getField('extension'); const filter = buildPhraseFilter(field!, 'jpg', indexPattern); - const result = handleNestedFilter(filter, indexPattern); + const result = fromNestedFilter(filter, indexPattern); expect(result).toBe(filter); }); @@ -75,14 +75,14 @@ describe('handleNestedFilter', function () { name: 'notarealfield', }; const filter = buildPhraseFilter(unrealField, 'jpg', indexPattern); - const result = handleNestedFilter(filter, indexPattern); + const result = fromNestedFilter(filter, indexPattern); expect(result).toBe(filter); }); it('should return filter untouched if no index pattern is provided', () => { const field = getField('extension'); const filter = buildPhraseFilter(field!, 'jpg', indexPattern); - const result = handleNestedFilter(filter); + const result = fromNestedFilter(filter); expect(result).toBe(filter); }); @@ -97,7 +97,7 @@ describe('handleNestedFilter', function () { 'logstash-*', 'foo' ); - const result = handleNestedFilter(filter); + const result = fromNestedFilter(filter); expect(result).toBe(filter); }); diff --git a/packages/kbn-es-query/src/es_query/handle_nested_filter.ts b/packages/kbn-es-query/src/es_query/from_nested_filter.ts similarity index 97% rename from packages/kbn-es-query/src/es_query/handle_nested_filter.ts rename to packages/kbn-es-query/src/es_query/from_nested_filter.ts index fb32d55e53a7e..f646c531f318e 100644 --- a/packages/kbn-es-query/src/es_query/handle_nested_filter.ts +++ b/packages/kbn-es-query/src/es_query/from_nested_filter.ts @@ -11,7 +11,7 @@ import { DataViewBase } from './types'; import { getDataViewFieldSubtypeNested } from '../utils'; /** @internal */ -export const handleNestedFilter = ( +export const fromNestedFilter = ( filter: Filter, indexPattern?: DataViewBase, config: { ignoreUnmapped?: boolean } = {} diff --git a/packages/kbn-es-query/src/es_query/handle_combined_filter.test.ts b/packages/kbn-es-query/src/es_query/handle_combined_filter.test.ts deleted file mode 100644 index 0cac2cb12f607..0000000000000 --- a/packages/kbn-es-query/src/es_query/handle_combined_filter.test.ts +++ /dev/null @@ -1,610 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { fields } from '../filters/stubs'; -import { DataViewBase } from './types'; -import { handleCombinedFilter } from './handle_combined_filter'; -import { - buildExistsFilter, - buildCombinedFilter, - buildPhraseFilter, - buildPhrasesFilter, - buildRangeFilter, -} from '../filters'; - -describe('#handleCombinedFilter', function () { - const indexPattern: DataViewBase = { - id: 'logstash-*', - fields, - title: 'dataView', - }; - - const getField = (fieldName: string) => { - const field = fields.find(({ name }) => fieldName === name); - if (!field) throw new Error(`field ${name} does not exist`); - return field; - }; - - it('Handles an empty list of filters', () => { - const filter = buildCombinedFilter([]); - const result = handleCombinedFilter(filter); - expect(result.query).toMatchInlineSnapshot(` - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [], - }, - } - `); - }); - - it('Handles a simple list of filters', () => { - const filters = [ - buildPhraseFilter(getField('extension'), 'value', indexPattern), - buildRangeFilter(getField('bytes'), { gte: 10 }, indexPattern), - buildExistsFilter(getField('machine.os'), indexPattern), - ]; - const filter = buildCombinedFilter(filters); - const result = handleCombinedFilter(filter); - expect(result.query).toMatchInlineSnapshot(` - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "bool": Object { - "filter": Array [ - Object { - "match_phrase": Object { - "extension": "value", - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "bytes": Object { - "gte": 10, - }, - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - Object { - "bool": Object { - "filter": Array [ - Object { - "exists": Object { - "field": "machine.os", - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - ], - }, - } - `); - }); - - it('Handles a combination of filters and filter arrays', () => { - const filters = [ - buildPhraseFilter(getField('extension'), 'value', indexPattern), - [ - buildRangeFilter(getField('bytes'), { gte: 10 }, indexPattern), - buildExistsFilter(getField('machine.os'), indexPattern), - ], - ]; - const filter = buildCombinedFilter(filters); - const result = handleCombinedFilter(filter); - expect(result.query).toMatchInlineSnapshot(` - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "bool": Object { - "filter": Array [ - Object { - "match_phrase": Object { - "extension": "value", - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "bytes": Object { - "gte": 10, - }, - }, - }, - Object { - "exists": Object { - "field": "machine.os", - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - ], - }, - } - `); - }); - - it('Handles nested COMBINED filters', () => { - const nestedCombinedFilter = buildCombinedFilter([ - buildPhraseFilter(getField('machine.os'), 'value', indexPattern), - buildPhraseFilter(getField('extension'), 'value', indexPattern), - ]); - const filters = [ - buildPhraseFilter(getField('extension'), 'value2', indexPattern), - nestedCombinedFilter, - buildRangeFilter(getField('bytes'), { gte: 10 }, indexPattern), - buildExistsFilter(getField('machine.os.raw'), indexPattern), - ]; - const filter = buildCombinedFilter(filters); - const result = handleCombinedFilter(filter); - expect(result.query).toMatchInlineSnapshot(` - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "bool": Object { - "filter": Array [ - Object { - "match_phrase": Object { - "extension": "value2", - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - Object { - "bool": Object { - "filter": Array [ - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "bool": Object { - "filter": Array [ - Object { - "match_phrase": Object { - "machine.os": "value", - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - Object { - "bool": Object { - "filter": Array [ - Object { - "match_phrase": Object { - "extension": "value", - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - ], - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "bytes": Object { - "gte": 10, - }, - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - Object { - "bool": Object { - "filter": Array [ - Object { - "exists": Object { - "field": "machine.os.raw", - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - ], - }, - } - `); - }); - - it('Handles negated sub-filters', () => { - const negatedFilter = buildPhrasesFilter(getField('extension'), ['tar', 'gz'], indexPattern); - negatedFilter.meta.negate = true; - - const filters = [ - [negatedFilter, buildPhraseFilter(getField('extension'), 'value', indexPattern)], - buildRangeFilter(getField('bytes'), { gte: 10 }, indexPattern), - buildExistsFilter(getField('machine.os'), indexPattern), - ]; - const filter = buildCombinedFilter(filters); - const result = handleCombinedFilter(filter); - expect(result.query).toMatchInlineSnapshot(` - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "bool": Object { - "filter": Array [ - Object { - "match_phrase": Object { - "extension": "value", - }, - }, - ], - "must": Array [], - "must_not": Array [ - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match_phrase": Object { - "extension": "tar", - }, - }, - Object { - "match_phrase": Object { - "extension": "gz", - }, - }, - ], - }, - }, - ], - "should": Array [], - }, - }, - Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "bytes": Object { - "gte": 10, - }, - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - Object { - "bool": Object { - "filter": Array [ - Object { - "exists": Object { - "field": "machine.os", - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - ], - }, - } - `); - }); - - it('Handles disabled filters within a filter array', () => { - const disabledFilter = buildPhraseFilter(getField('ssl'), false, indexPattern); - disabledFilter.meta.disabled = true; - const filters = [ - buildPhraseFilter(getField('extension'), 'value', indexPattern), - [disabledFilter, buildRangeFilter(getField('bytes'), { gte: 10 }, indexPattern)], - buildExistsFilter(getField('machine.os'), indexPattern), - ]; - const filter = buildCombinedFilter(filters); - const result = handleCombinedFilter(filter); - expect(result.query).toMatchInlineSnapshot(` - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "bool": Object { - "filter": Array [ - Object { - "match_phrase": Object { - "extension": "value", - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "bytes": Object { - "gte": 10, - }, - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - Object { - "bool": Object { - "filter": Array [ - Object { - "exists": Object { - "field": "machine.os", - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - ], - }, - } - `); - }); - - it('Handles complex-nested filters with ANDs and ORs', () => { - const filters = [ - [ - buildPhrasesFilter(getField('extension'), ['tar', 'gz'], indexPattern), - buildPhraseFilter(getField('ssl'), false, indexPattern), - buildCombinedFilter([ - buildPhraseFilter(getField('extension'), 'value', indexPattern), - buildRangeFilter(getField('bytes'), { gte: 10 }, indexPattern), - ]), - buildExistsFilter(getField('machine.os'), indexPattern), - ], - buildPhrasesFilter(getField('machine.os.keyword'), ['foo', 'bar'], indexPattern), - ]; - const filter = buildCombinedFilter(filters); - const result = handleCombinedFilter(filter); - expect(result.query).toMatchInlineSnapshot(` - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "bool": Object { - "filter": Array [ - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match_phrase": Object { - "extension": "tar", - }, - }, - Object { - "match_phrase": Object { - "extension": "gz", - }, - }, - ], - }, - }, - Object { - "match_phrase": Object { - "ssl": false, - }, - }, - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "bool": Object { - "filter": Array [ - Object { - "match_phrase": Object { - "extension": "value", - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "bytes": Object { - "gte": 10, - }, - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - ], - }, - }, - Object { - "exists": Object { - "field": "machine.os", - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - Object { - "bool": Object { - "filter": Array [ - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match_phrase": Object { - "machine.os.keyword": "foo", - }, - }, - Object { - "match_phrase": Object { - "machine.os.keyword": "bar", - }, - }, - ], - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - ], - }, - } - `); - }); - - it('Preserves filter properties', () => { - const filters = [ - buildPhraseFilter(getField('extension'), 'value', indexPattern), - buildRangeFilter(getField('bytes'), { gte: 10 }, indexPattern), - buildExistsFilter(getField('machine.os'), indexPattern), - ]; - const filter = buildCombinedFilter(filters); - const { query, ...rest } = handleCombinedFilter(filter); - expect(rest).toMatchInlineSnapshot(` - Object { - "$state": Object { - "store": "appState", - }, - "meta": Object { - "alias": null, - "disabled": false, - "index": undefined, - "negate": false, - "params": Array [ - Object { - "meta": Object { - "index": "logstash-*", - }, - "query": Object { - "match_phrase": Object { - "extension": "value", - }, - }, - }, - Object { - "meta": Object { - "field": "bytes", - "index": "logstash-*", - "params": Object {}, - }, - "query": Object { - "range": Object { - "bytes": Object { - "gte": 10, - }, - }, - }, - }, - Object { - "meta": Object { - "index": "logstash-*", - }, - "query": Object { - "exists": Object { - "field": "machine.os", - }, - }, - }, - ], - "type": "combined", - }, - } - `); - }); -}); diff --git a/packages/kbn-es-query/src/es_query/handle_combined_filter.ts b/packages/kbn-es-query/src/es_query/handle_combined_filter.ts deleted file mode 100644 index a9daf3fc4f33b..0000000000000 --- a/packages/kbn-es-query/src/es_query/handle_combined_filter.ts +++ /dev/null @@ -1,41 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Filter, FilterItem, isCombinedFilter } from '../filters'; -import { DataViewBase } from './types'; -import { buildQueryFromFilters, EsQueryFiltersConfig } from './from_filters'; - -/** @internal */ -export const handleCombinedFilter = ( - filter: Filter, - inputDataViews?: DataViewBase | DataViewBase[], - options: EsQueryFiltersConfig = {} -): Filter => { - if (!isCombinedFilter(filter)) return filter; - const { params } = filter.meta; - const should = params.map((subFilter) => { - const subFilters = Array.isArray(subFilter) ? subFilter : [subFilter]; - return { bool: buildQueryFromFilters(flattenFilters(subFilters), inputDataViews, options) }; - }); - return { - ...filter, - query: { - bool: { - should, - minimum_should_match: 1, - }, - }, - }; -}; - -function flattenFilters(filters: FilterItem[]): Filter[] { - return filters.reduce((result, filter) => { - if (Array.isArray(filter)) return [...result, ...flattenFilters(filter)]; - return [...result, filter]; - }, []); -} diff --git a/packages/kbn-es-query/src/es_query/index.ts b/packages/kbn-es-query/src/es_query/index.ts index 5f14b1f03769e..5d3e44f597cbd 100644 --- a/packages/kbn-es-query/src/es_query/index.ts +++ b/packages/kbn-es-query/src/es_query/index.ts @@ -19,6 +19,7 @@ export { getAggregateQueryMode, getIndexPatternFromSQLQuery, } from './es_query_sql'; +export { fromCombinedFilter } from './from_combined_filter'; export type { IFieldSubType, BoolQuery, diff --git a/packages/kbn-es-query/src/es_query/migrate_filter.ts b/packages/kbn-es-query/src/es_query/migrate_filter.ts index 0a8aa8ee69f80..809b6714681d3 100644 --- a/packages/kbn-es-query/src/es_query/migrate_filter.ts +++ b/packages/kbn-es-query/src/es_query/migrate_filter.ts @@ -64,7 +64,7 @@ export function migrateFilter(filter: Filter, indexPattern?: DataViewBase) { } if (!filter.query) { - filter.query = {}; + filter = { ...filter, query: {} }; } else { // handle the case where .query already exists and filter has other top level keys on there filter = pick(filter, ['meta', 'query', '$state']); diff --git a/packages/kbn-es-query/src/filters/build_filters/combined_filter.ts b/packages/kbn-es-query/src/filters/build_filters/combined_filter.ts index 4054f25ce45f6..311a360d9cb1e 100644 --- a/packages/kbn-es-query/src/filters/build_filters/combined_filter.ts +++ b/packages/kbn-es-query/src/filters/build_filters/combined_filter.ts @@ -6,22 +6,24 @@ * Side Public License, v 1. */ -import { Filter, FilterMeta, FILTERS } from './types'; -import { buildEmptyFilter } from './build_empty_filter'; +import { Filter, FilterMeta, FILTERS, FilterStateStore } from './types'; +import { DataViewBase } from '../../es_query'; /** - * Each item in an COMBINED filter may represent either one filter (to be ORed) or an array of filters (ANDed together before - * becoming part of the OR clause). * @public */ -export type FilterItem = Filter | FilterItem[]; +export enum BooleanRelation { + AND = 'AND', + OR = 'OR', +} /** * @public */ export interface CombinedFilterMeta extends FilterMeta { type: typeof FILTERS.COMBINED; - params: FilterItem[]; + relation: BooleanRelation; + params: Filter[]; } /** @@ -38,20 +40,38 @@ export function isCombinedFilter(filter: Filter): filter is CombinedFilter { return filter?.meta?.type === FILTERS.COMBINED; } +const cleanUpFilter = (filter: Filter) => { + const { $state, meta, ...cleanedUpFilter } = filter; + const { alias, disabled, ...cleanedUpMeta } = meta; + return { ...cleanedUpFilter, meta: cleanedUpMeta }; +}; + /** - * Builds an COMBINED filter. An COMBINED filter is a filter with multiple sub-filters. Each sub-filter (FilterItem) represents a - * condition. - * @param filters An array of CombinedFilterItem + * Builds an COMBINED filter. An COMBINED filter is a filter with multiple sub-filters. Each sub-filter (FilterItem) + * represents a condition. + * @param relation The type of relation with which to combine the filters (AND/OR) + * @param filters An array of sub-filters * @public */ -export function buildCombinedFilter(filters: FilterItem[]): CombinedFilter { - const filter = buildEmptyFilter(false); +export function buildCombinedFilter( + relation: BooleanRelation, + filters: Filter[], + indexPattern: DataViewBase, + disabled: FilterMeta['disabled'] = false, + negate: FilterMeta['negate'] = false, + alias?: FilterMeta['alias'], + store: FilterStateStore = FilterStateStore.APP_STATE +): CombinedFilter { return { - ...filter, + $state: { store }, meta: { - ...filter.meta, type: FILTERS.COMBINED, - params: filters, + relation, + params: filters.map(cleanUpFilter), + index: indexPattern.id, + disabled, + negate, + alias, }, }; } diff --git a/packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts b/packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts index cfb812fd6cd23..a7328758ec429 100644 --- a/packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts +++ b/packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts @@ -6,8 +6,15 @@ * Side Public License, v 1. */ -import { compareFilters, COMPARE_ALL_OPTIONS } from './compare_filters'; -import { buildEmptyFilter, buildQueryFilter, FilterStateStore } from '..'; +import { COMPARE_ALL_OPTIONS, compareFilters } from './compare_filters'; +import { + BooleanRelation, + buildCombinedFilter, + buildEmptyFilter, + buildQueryFilter, + FilterStateStore, +} from '..'; +import { DataViewBase } from '@kbn/es-query'; describe('filter manager utilities', () => { describe('compare filters', () => { @@ -177,5 +184,68 @@ describe('filter manager utilities', () => { expect(compareFilters([f1], [f2], { index: true })).toBeFalsy(); }); + + test('should compare two AND filters as the same', () => { + const dataView: DataViewBase = { + id: 'logstash-*', + fields: [ + { + name: 'bytes', + type: 'number', + scripted: false, + }, + ], + title: 'dataView', + }; + + const f1 = buildQueryFilter({ query_string: { query: 'apache' } }, dataView.id!, ''); + const f2 = buildQueryFilter({ query_string: { query: 'apache' } }, dataView.id!, ''); + const f3 = buildCombinedFilter(BooleanRelation.AND, [f1, f2], dataView); + const f4 = buildCombinedFilter(BooleanRelation.AND, [f1, f2], dataView); + + expect(compareFilters([f3], [f4])).toBeTruthy(); + }); + + test('should compare an AND and OR filter as different', () => { + const dataView: DataViewBase = { + id: 'logstash-*', + fields: [ + { + name: 'bytes', + type: 'number', + scripted: false, + }, + ], + title: 'dataView', + }; + + const f1 = buildQueryFilter({ query_string: { query: 'apache' } }, dataView.id!, ''); + const f2 = buildQueryFilter({ query_string: { query: 'apache' } }, dataView.id!, ''); + const f3 = buildCombinedFilter(BooleanRelation.AND, [f1, f2], dataView); + const f4 = buildCombinedFilter(BooleanRelation.OR, [f1, f2], dataView); + + expect(compareFilters([f3], [f4])).toBeFalsy(); + }); + + test('should compare two different combined filters as different', () => { + const dataView: DataViewBase = { + id: 'logstash-*', + fields: [ + { + name: 'bytes', + type: 'number', + scripted: false, + }, + ], + title: 'dataView', + }; + + const f1 = buildQueryFilter({ query_string: { query: 'apache' } }, dataView.id!, ''); + const f2 = buildQueryFilter({ query_string: { query: 'apaches' } }, dataView.id!, ''); + const f3 = buildCombinedFilter(BooleanRelation.AND, [f1], dataView); + const f4 = buildCombinedFilter(BooleanRelation.AND, [f2], dataView); + + expect(compareFilters([f3], [f4])).toBeFalsy(); + }); }); }); diff --git a/packages/kbn-es-query/src/filters/helpers/compare_filters.ts b/packages/kbn-es-query/src/filters/helpers/compare_filters.ts index ffc7461bc6cdd..a608720b24ec0 100644 --- a/packages/kbn-es-query/src/filters/helpers/compare_filters.ts +++ b/packages/kbn-es-query/src/filters/helpers/compare_filters.ts @@ -8,6 +8,7 @@ import { defaults, isEqual, omit, map } from 'lodash'; import type { FilterMeta, Filter } from '../build_filters'; +import { isCombinedFilter } from '../build_filters'; /** @public */ export interface FilterCompareOptions { @@ -30,13 +31,21 @@ export const COMPARE_ALL_OPTIONS: FilterCompareOptions = { alias: true, }; +// Combined filters include sub-filters in the `meta` property and the relation type in the `relation` property, so +// they should never be excluded in the comparison +const removeRequiredAttributes = (excludedAttributes: string[]) => + excludedAttributes.filter((attribute) => !['meta', 'relation'].includes(attribute)); + const mapFilter = ( filter: Filter, comparators: FilterCompareOptions, excludedAttributes: string[] ) => { - const cleaned: FilterMeta = omit(filter, excludedAttributes) as FilterMeta; + const attrsToExclude = isCombinedFilter(filter) + ? removeRequiredAttributes(excludedAttributes) + : excludedAttributes; + const cleaned: FilterMeta = omit(filter, attrsToExclude) as FilterMeta; if (comparators.index) cleaned.index = filter.meta?.index; if (comparators.negate) cleaned.negate = filter.meta && Boolean(filter.meta.negate); if (comparators.disabled) cleaned.disabled = filter.meta && Boolean(filter.meta.disabled); diff --git a/packages/kbn-es-query/src/filters/helpers/index.ts b/packages/kbn-es-query/src/filters/helpers/index.ts index 815ae23dd3172..2628e3829925a 100644 --- a/packages/kbn-es-query/src/filters/helpers/index.ts +++ b/packages/kbn-es-query/src/filters/helpers/index.ts @@ -9,6 +9,7 @@ export * from './compare_filters'; export * from './dedup_filters'; export * from './uniq_filters'; +export * from './update_filter'; export * from './meta_filter'; export * from './only_disabled'; export * from './extract_time_filter'; diff --git a/packages/kbn-es-query/src/filters/helpers/update_filter.ts b/packages/kbn-es-query/src/filters/helpers/update_filter.ts new file mode 100644 index 0000000000000..ed69cb72aa164 --- /dev/null +++ b/packages/kbn-es-query/src/filters/helpers/update_filter.ts @@ -0,0 +1,138 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { identity, pickBy } from 'lodash'; + +import type { Filter, FilterMeta } from '..'; + +type FilterOperator = Pick; + +export const updateFilter = ( + filter: Filter, + field?: string, + operator?: FilterOperator, + params?: Filter['meta']['params'] +) => { + if (!field || !operator) { + return updateField(filter, field); + } + + if (operator.type === 'exists') { + return updateWithExistsOperator(filter, operator); + } + if (operator.type === 'range') { + return updateWithRangeOperator(filter, operator, params, field); + } + if (Array.isArray(params)) { + return updateWithIsOneOfOperator(filter, operator, params); + } + + return updateWithIsOperator(filter, operator, params); +}; + +function updateField(filter: Filter, field?: string) { + return { + ...filter, + meta: { + ...filter.meta, + key: field, + // @todo: check why we need to pass "key" and "field" with the same data + field, + params: { query: undefined }, + value: undefined, + type: undefined, + }, + query: undefined, + }; +} + +function updateWithExistsOperator(filter: Filter, operator?: FilterOperator) { + return { + ...filter, + meta: { + ...filter.meta, + negate: operator?.negate, + type: operator?.type, + params: undefined, + value: 'exists', + }, + query: { exists: { field: filter.meta.key } }, + }; +} + +function updateWithIsOperator( + filter: Filter, + operator?: FilterOperator, + params?: Filter['meta']['params'] +) { + return { + ...filter, + meta: { + ...filter.meta, + negate: operator?.negate, + type: operator?.type, + params: { ...filter.meta.params, query: params }, + }, + query: { match_phrase: { [filter.meta.key!]: params ?? '' } }, + }; +} + +function updateWithRangeOperator( + filter: Filter, + operator: FilterOperator, + rawParams: Array, + field: string +) { + const params = { + ...filter.meta.params, + ...pickBy(rawParams, identity), + }; + + params.gte = params.from; + params.lt = params.to; + + const updatedFilter = { + ...filter, + meta: { + ...filter.meta, + negate: operator?.negate, + type: operator?.type, + params, + }, + query: { + range: { + [field]: params, + }, + }, + }; + + return updatedFilter; +} + +function updateWithIsOneOfOperator( + filter: Filter, + operator?: FilterOperator, + params?: Array +) { + return { + ...filter, + meta: { + ...filter.meta, + negate: operator?.negate, + type: operator?.type, + params, + }, + query: { + bool: { + minimum_should_match: 1, + ...filter!.query?.should, + should: params?.map((param) => ({ match_phrase: { [filter.meta.key!]: param } })), + }, + }, + }; +} diff --git a/packages/kbn-es-query/src/filters/index.ts b/packages/kbn-es-query/src/filters/index.ts index 93efb9b1cd61f..9f5b376e2be65 100644 --- a/packages/kbn-es-query/src/filters/index.ts +++ b/packages/kbn-es-query/src/filters/index.ts @@ -17,6 +17,7 @@ export { isFilter, isFilters, pinFilter, + updateFilter, isFilterPinned, onlyDisabledFiltersChanged, enableFilter, @@ -57,6 +58,7 @@ export { isScriptedPhraseFilter, isScriptedRangeFilter, getFilterParams, + BooleanRelation, } from './build_filters'; export type { @@ -79,7 +81,6 @@ export type { QueryStringFilter, CombinedFilter, CombinedFilterMeta, - FilterItem, } from './build_filters'; export { FilterStateStore, FILTERS } from './build_filters/types'; diff --git a/src/plugins/data/common/search/expressions/filters_to_ast.ts b/src/plugins/data/common/search/expressions/filters_to_ast.ts index d139ac664ed48..8bc6456078f10 100644 --- a/src/plugins/data/common/search/expressions/filters_to_ast.ts +++ b/src/plugins/data/common/search/expressions/filters_to_ast.ts @@ -6,13 +6,16 @@ * Side Public License, v 1. */ -import { Filter } from '@kbn/es-query'; +import { Filter, FILTERS, fromCombinedFilter } from '@kbn/es-query'; import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/common'; import { ExpressionFunctionKibanaFilter } from './kibana_filter'; export const filtersToAst = (filters: Filter[] | Filter) => { return (Array.isArray(filters) ? filters : [filters]).map((filter) => { - const { meta, $state, query, ...restOfFilters } = filter; + const filterWithQuery = + filter.meta.type === FILTERS.COMBINED ? fromCombinedFilter(filter) : filter; + const { meta, $state, query, ...restOfFilters } = filterWithQuery; + return buildExpression([ buildExpressionFunction('kibanaFilter', { query: JSON.stringify(query || restOfFilters), diff --git a/src/plugins/data/common/search/expressions/kibana_context.ts b/src/plugins/data/common/search/expressions/kibana_context.ts index 6183484a57b46..382589b113959 100644 --- a/src/plugins/data/common/search/expressions/kibana_context.ts +++ b/src/plugins/data/common/search/expressions/kibana_context.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import { uniqBy } from 'lodash'; +import { isEqual, uniqBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition, ExecutionContext } from '@kbn/expressions-plugin/common'; import { Adapters } from '@kbn/inspector-plugin/common'; -import { Filter } from '@kbn/es-query'; +import { Filter, fromCombinedFilter } from '@kbn/es-query'; import { Query, uniqFilters } from '@kbn/es-query'; import { unboxExpressionValue } from '@kbn/expressions-plugin/common'; import { SavedObjectReference } from '@kbn/core/types'; @@ -124,10 +124,9 @@ export const getKibanaContextFn = ( const timeRange = args.timeRange || input?.timeRange; let queries = mergeQueries(input?.query, args?.q?.filter(Boolean) || []); - let filters = [ - ...(input?.filters || []), - ...((args?.filters?.map(unboxExpressionValue) || []) as Filter[]), - ]; + const filterFromArgs = (args?.filters?.map(unboxExpressionValue) || []) as Filter[]; + + let filters = [...(input?.filters || [])]; if (args.savedSearchId) { const obj = await savedObjectsClient.get('search', args.savedSearchId); @@ -141,6 +140,14 @@ export const getKibanaContextFn = ( filters = [...filters, ...(Array.isArray(filter) ? filter : [filter])]; } } + const uniqueArgFilters = filterFromArgs.filter( + (argF) => + !filters.some((f) => { + return isEqual(fromCombinedFilter(f).query, argF.query); + }) + ); + + filters = [...filters, ...uniqueArgFilters]; return { type: 'kibana_context', diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 717324c647d42..14b2e364878ef 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -1013,6 +1013,7 @@ export class SearchSource { const filters = ( typeof searchRequest.filters === 'function' ? searchRequest.filters() : searchRequest.filters ) as Filter[] | Filter | undefined; + const ast = buildExpression([ buildExpressionFunction('kibana_context', { q: query?.map(queryToAst), diff --git a/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts b/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts index 5543e0071b4d0..6a8f5d895728a 100644 --- a/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts +++ b/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts @@ -16,18 +16,20 @@ import { isScriptedPhraseFilter, isScriptedRangeFilter, getFilterField, + DataViewBase, + DataViewFieldBase, } from '@kbn/es-query'; import { getPhraseDisplayValue } from './mappers/map_phrase'; import { getPhrasesDisplayValue } from './mappers/map_phrases'; import { getRangeDisplayValue } from './mappers/map_range'; import { getIndexPatternFromFilter } from './get_index_pattern_from_filter'; -function getValueFormatter(indexPattern?: DataView, key?: string) { +function getValueFormatter(indexPattern?: DataViewBase | DataView, key?: string) { // checking getFormatterForField exists because there is at least once case where an index pattern // is an object rather than an IndexPattern class - if (!indexPattern || !indexPattern.getFormatterForField || !key) return; + if (!indexPattern || !('getFormatterForField' in indexPattern) || !key) return; - const field = indexPattern.fields.find((f: DataViewField) => f.name === key); + const field = indexPattern.fields.find((f) => f.name === key); if (!field) { throw new Error( i18n.translate('data.filter.filterBar.fieldNotFound', { @@ -39,18 +41,24 @@ function getValueFormatter(indexPattern?: DataView, key?: string) { return indexPattern.getFormatterForField(field); } -export function getFieldDisplayValueFromFilter(filter: Filter, indexPatterns: DataView[]): string { - const indexPattern = getIndexPatternFromFilter(filter, indexPatterns); +export function getFieldDisplayValueFromFilter( + filter: Filter, + indexPatterns: DataView[] | DataViewBase[] +): string { + const indexPattern = getIndexPatternFromFilter(filter, indexPatterns); if (!indexPattern) return ''; const fieldName = getFilterField(filter); if (!fieldName) return ''; - const field = indexPattern.fields.find((f: DataViewField) => f.name === fieldName); - return field?.customLabel ?? ''; + const field = indexPattern.fields.find( + (f: DataViewFieldBase | DataViewField) => f.name === fieldName + ); + + return field && 'customLabel' in field ? (field as DataViewField).customLabel ?? '' : ''; } -export function getDisplayValueFromFilter(filter: Filter, indexPatterns: DataView[]): string { +export function getDisplayValueFromFilter(filter: Filter, indexPatterns: DataViewBase[]): string { const indexPattern = getIndexPatternFromFilter(filter, indexPatterns); const fieldName = getFilterField(filter); const valueFormatter = getValueFormatter(indexPattern, fieldName); diff --git a/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts b/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts index c4da427ed9780..6e345cbda9d9a 100644 --- a/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts +++ b/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts @@ -6,12 +6,11 @@ * Side Public License, v 1. */ -import { DataView } from '@kbn/data-views-plugin/public'; -import { Filter } from '@kbn/es-query'; +import { Filter, DataViewBase } from '@kbn/es-query'; -export function getIndexPatternFromFilter( +export function getIndexPatternFromFilter( filter: Filter, - indexPatterns: DataView[] -): DataView | undefined { + indexPatterns: T[] +): T | undefined { return indexPatterns.find((indexPattern) => indexPattern.id === filter.meta.index); } diff --git a/src/plugins/data/public/query/filter_manager/lib/map_filter.ts b/src/plugins/data/public/query/filter_manager/lib/map_filter.ts index 73144e14bc150..eb428eb8d8af3 100644 --- a/src/plugins/data/public/query/filter_manager/lib/map_filter.ts +++ b/src/plugins/data/public/query/filter_manager/lib/map_filter.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -import { reduceRight } from 'lodash'; +import { cloneDeep, reduceRight } from 'lodash'; import { Filter } from '@kbn/es-query'; +import { mapCombined } from './mappers/map_combined'; import { mapSpatialFilter } from './mappers/map_spatial_filter'; import { mapMatchAll } from './mappers/map_match_all'; import { mapPhrase } from './mappers/map_phrase'; @@ -37,6 +38,7 @@ export function mapFilter(filter: Filter) { // that either handles the mapping operation or not // and add it here. ProTip: These are executed in order listed const mappers = [ + mapCombined, mapSpatialFilter, mapMatchAll, mapRange, @@ -59,19 +61,20 @@ export function mapFilter(filter: Filter) { noop ); - const mapped = mapFn(filter); + const mappedFilter = cloneDeep(filter); + const mapped = mapFn(mappedFilter); // Map the filter into an object with the key and value exposed so it's // easier to work with in the template - filter.meta = filter.meta || {}; - filter.meta.type = mapped.type; - filter.meta.key = mapped.key; + mappedFilter.meta = filter.meta || {}; + mappedFilter.meta.type = mapped.type; + mappedFilter.meta.key = mapped.key; // Display value or formatter function. - filter.meta.value = mapped.value; - filter.meta.params = mapped.params; - filter.meta.disabled = Boolean(filter.meta.disabled); - filter.meta.negate = Boolean(filter.meta.negate); - filter.meta.alias = filter.meta.alias || null; + mappedFilter.meta.value = mapped.value; + mappedFilter.meta.params = mapped.params; + mappedFilter.meta.disabled = Boolean(mappedFilter.meta.disabled); + mappedFilter.meta.negate = Boolean(mappedFilter.meta.negate); + mappedFilter.meta.alias = mappedFilter.meta.alias || null; - return filter; + return mappedFilter; } diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_combined.test.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_combined.test.ts new file mode 100644 index 0000000000000..75376077bae8c --- /dev/null +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_combined.test.ts @@ -0,0 +1,76 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DataView } from '@kbn/data-views-plugin/common'; +import { + BooleanRelation, + buildEmptyFilter, + buildCombinedFilter, + FilterMeta, + RangeFilter, +} from '@kbn/es-query'; +import { mapCombined } from './map_combined'; + +describe('filter manager utilities', () => { + describe('mapCombined()', () => { + test('should throw if not a combinedFilter', async () => { + const filter = buildEmptyFilter(true); + try { + mapCombined(filter); + } catch (e) { + expect(e).toBe(filter); + } + }); + + test('should call mapFilter for sub-filters', async () => { + const rangeFilter = { + meta: { index: 'logstash-*' } as FilterMeta, + query: { range: { bytes: { lt: 2048, gt: 1024 } } }, + } as RangeFilter; + const filter = buildCombinedFilter(BooleanRelation.AND, [rangeFilter], { + id: 'logstash-*', + } as DataView); + const result = mapCombined(filter); + + expect(result).toMatchInlineSnapshot(` + Object { + "key": undefined, + "params": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "index": "logstash-*", + "key": "bytes", + "negate": false, + "params": Object { + "gt": 1024, + "lt": 2048, + }, + "type": "range", + "value": Object { + "gt": 1024, + "lt": 2048, + }, + }, + "query": Object { + "range": Object { + "bytes": Object { + "gt": 1024, + "lt": 2048, + }, + }, + }, + }, + ], + "type": "combined", + } + `); + }); + }); +}); diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_combined.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_combined.ts new file mode 100644 index 0000000000000..d667cc91d4a16 --- /dev/null +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_combined.ts @@ -0,0 +1,24 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Filter, isCombinedFilter } from '@kbn/es-query'; +import { mapFilter } from '../map_filter'; + +export const mapCombined = (filter: Filter) => { + if (!isCombinedFilter(filter)) { + throw filter; + } + + const { type, key, params } = filter.meta; + + return { + type, + key, + params: params.map(mapFilter), + }; +}; diff --git a/src/plugins/unified_search/jest.config.js b/src/plugins/unified_search/jest.config.js index 783631ba05d5c..c5651aff97e54 100644 --- a/src/plugins/unified_search/jest.config.js +++ b/src/plugins/unified_search/jest.config.js @@ -13,4 +13,5 @@ module.exports = { coverageDirectory: '/target/kibana-coverage/jest/src/plugins/unified_search', coverageReporters: ['text', 'html'], collectCoverageFrom: ['/src/plugins/unified_search/public/**/*.{ts,tsx}'], + setupFiles: ['jest-canvas-mock'], }; diff --git a/src/plugins/unified_search/public/apply_filters/apply_filter_popover_content.tsx b/src/plugins/unified_search/public/apply_filters/apply_filter_popover_content.tsx index 9ffe7bc32a0aa..cb61703fed5ac 100644 --- a/src/plugins/unified_search/public/apply_filters/apply_filter_popover_content.tsx +++ b/src/plugins/unified_search/public/apply_filters/apply_filter_popover_content.tsx @@ -25,8 +25,8 @@ import { getFieldDisplayValueFromFilter, } from '@kbn/data-plugin/public'; import type { Filter } from '@kbn/es-query'; -import { DataView } from '@kbn/data-views-plugin/public'; -import { FilterLabel } from '../filter_bar'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { FilterContent } from '../filter_badge'; interface Props { filters: Filter[]; @@ -58,7 +58,7 @@ export default class ApplyFiltersPopoverContent extends Component private getLabel = (filter: Filter) => { const valueLabel = getDisplayValueFromFilter(filter, this.props.indexPatterns); const fieldLabel = getFieldDisplayValueFromFilter(filter, this.props.indexPatterns); - return ; + return ; }; public render() { diff --git a/src/plugins/unified_search/public/filter_badge/filter_badge.styles.ts b/src/plugins/unified_search/public/filter_badge/filter_badge.styles.ts new file mode 100644 index 0000000000000..424bca9378bce --- /dev/null +++ b/src/plugins/unified_search/public/filter_badge/filter_badge.styles.ts @@ -0,0 +1,30 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { css } from '@emotion/css'; +import { euiThemeVars } from '@kbn/ui-theme'; +import type { EuiThemeComputed } from '@elastic/eui'; + +export const badgePaddingCss = (euiTheme: EuiThemeComputed) => css` + padding: calc(${euiTheme.size.xs} + ${euiTheme.size.xxs}); +`; + +export const marginLeftLabelCss = (euiTheme: EuiThemeComputed) => css` + margin-left: ${euiTheme.size.xs}; +`; + +export const bracketColorCss = css` + color: ${euiThemeVars.euiColorPrimary}; +`; + +export const conditionSpacesCss = (euiTheme: EuiThemeComputed) => css` + margin-inline: -${euiTheme.size.xs}; +`; + +export const conditionCss = css` + ${bracketColorCss} +`; diff --git a/src/plugins/unified_search/public/filter_badge/filter_badge.tsx b/src/plugins/unified_search/public/filter_badge/filter_badge.tsx new file mode 100644 index 0000000000000..d6c1d8303fcb5 --- /dev/null +++ b/src/plugins/unified_search/public/filter_badge/filter_badge.tsx @@ -0,0 +1,84 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiBadge, EuiTextColor, useEuiTheme } from '@elastic/eui'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { Filter } from '@kbn/es-query'; +import { isCombinedFilter } from '@kbn/es-query'; +import { FilterBadgeGroup } from './filter_badge_group'; +import type { FilterLabelStatus } from '../filter_bar/filter_item/filter_item'; +import { badgePaddingCss, marginLeftLabelCss } from './filter_badge.styles'; +import { strings } from './i18n'; + +export interface FilterBadgeProps { + filter: Filter; + dataViews: DataView[]; + valueLabel: string; + hideAlias?: boolean; + filterLabelStatus: FilterLabelStatus; +} + +function FilterBadge({ + filter, + dataViews, + valueLabel, + hideAlias, + filterLabelStatus, + ...rest +}: FilterBadgeProps) { + const { euiTheme } = useEuiTheme(); + + if (!dataViews.length) { + return null; + } + + const prefixText = filter.meta.negate ? ` ${strings.getNotLabel()}` : ''; + + const prefix = + filter.meta.negate && !filter.meta.disabled ? ( + {prefixText} + ) : ( + prefixText + ); + + const filterLabelValue = {valueLabel}; + + return ( + + {!hideAlias && filter.meta.alias !== null ? ( + <> + + {prefix} + {filter.meta.alias} + {filterLabelStatus && <>: {filterLabelValue}} + + + ) : ( +
+ {isCombinedFilter(filter) && prefix} + +
+ )} +
+ ); +} + +// React.lazy support +// eslint-disable-next-line import/no-default-export +export default FilterBadge; diff --git a/src/plugins/unified_search/public/filter_badge/filter_badge_error_boundary.tsx b/src/plugins/unified_search/public/filter_badge/filter_badge_error_boundary.tsx new file mode 100644 index 0000000000000..fa01272f3fae9 --- /dev/null +++ b/src/plugins/unified_search/public/filter_badge/filter_badge_error_boundary.tsx @@ -0,0 +1,43 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { Component } from 'react'; +import { FilterBadgeInvalidPlaceholder } from './filter_badge_invalid'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface FilterBadgeErrorBoundaryProps {} + +interface FilterBadgeErrorBoundaryState { + hasError: boolean; +} + +export class FilterBadgeErrorBoundary extends Component< + FilterBadgeErrorBoundaryProps, + FilterBadgeErrorBoundaryState +> { + constructor(props: FilterBadgeErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError() { + return { hasError: true }; + } + + componentWillReceiveProps() { + this.setState({ hasError: false }); + } + + render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return ; + } + + return this.props.children; + } +} diff --git a/src/plugins/unified_search/public/filter_badge/filter_badge_expression.tsx b/src/plugins/unified_search/public/filter_badge/filter_badge_expression.tsx new file mode 100644 index 0000000000000..d01f7977c6b98 --- /dev/null +++ b/src/plugins/unified_search/public/filter_badge/filter_badge_expression.tsx @@ -0,0 +1,87 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { getDisplayValueFromFilter, getFieldDisplayValueFromFilter } from '@kbn/data-plugin/public'; +import type { Filter, DataViewBase } from '@kbn/es-query'; +import { EuiTextColor } from '@elastic/eui'; +import { FilterBadgeGroup } from './filter_badge_group'; +import { FilterContent } from './filter_content'; +import { getBooleanRelationType } from '../utils'; +import { FilterBadgeInvalidPlaceholder } from './filter_badge_invalid'; +import { bracketColorCss } from './filter_badge.styles'; + +export interface FilterBadgeExpressionProps { + filter: Filter; + shouldShowBrackets?: boolean; + dataViews: DataViewBase[]; + filterLabelStatus?: string; +} + +interface FilterBadgeContentProps { + filter: Filter; + dataViews: DataViewBase[]; + filterLabelStatus?: string; +} + +const FilterBadgeContent = ({ filter, dataViews, filterLabelStatus }: FilterBadgeContentProps) => { + const valueLabel = filterLabelStatus || getDisplayValueFromFilter(filter, dataViews); + + const fieldLabel = getFieldDisplayValueFromFilter(filter, dataViews); + + if (!valueLabel || !filter) { + return ; + } + + return ( + + ); +}; + +export function FilterExpressionBadge({ + filter, + shouldShowBrackets, + dataViews, + filterLabelStatus, +}: FilterBadgeExpressionProps) { + const conditionalOperationType = getBooleanRelationType(filter); + + return conditionalOperationType ? ( + <> + {shouldShowBrackets && ( + + ( + + )} + + {shouldShowBrackets && ( + + ) + + )} + + ) : ( + + + + ); +} diff --git a/src/plugins/unified_search/public/filter_badge/filter_badge_group.tsx b/src/plugins/unified_search/public/filter_badge/filter_badge_group.tsx new file mode 100644 index 0000000000000..570cc4af5b841 --- /dev/null +++ b/src/plugins/unified_search/public/filter_badge/filter_badge_group.tsx @@ -0,0 +1,61 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { Filter, BooleanRelation, DataViewBase } from '@kbn/es-query'; +import { EuiTextColor } from '@elastic/eui'; +import { FilterBadgeErrorBoundary } from './filter_badge_error_boundary'; +import { FilterExpressionBadge } from './filter_badge_expression'; +import { conditionCss } from './filter_badge.styles'; + +export interface FilterBadgeGroupProps { + filters: Filter[]; + dataViews: DataViewBase[]; + filterLabelStatus?: string; + shouldShowBrackets?: boolean; + booleanRelation?: BooleanRelation; +} + +const BooleanRelationDelimiter = ({ conditional }: { conditional: BooleanRelation }) => { + /** + * Spaces have been added to make the title readable. + */ + return {` ${conditional} `}; +}; + +export function FilterBadgeGroup({ + filters, + dataViews, + filterLabelStatus, + booleanRelation, + shouldShowBrackets = true, +}: FilterBadgeGroupProps) { + return ( + + {filters.map((filter, index, filterArr) => { + const showRelationDelimiter = booleanRelation && index + 1 < filterArr.length; + const showBrackets = shouldShowBrackets && (filter.meta.negate || filterArr.length > 1); + return ( + <> + + {showRelationDelimiter && } + + ); + })} + + ); +} + +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default FilterBadgeGroup; diff --git a/src/plugins/unified_search/public/filter_badge/filter_badge_invalid.tsx b/src/plugins/unified_search/public/filter_badge/filter_badge_invalid.tsx new file mode 100644 index 0000000000000..503053da1ad0f --- /dev/null +++ b/src/plugins/unified_search/public/filter_badge/filter_badge_invalid.tsx @@ -0,0 +1,23 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { EuiBadge, useEuiTheme } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +export const FilterBadgeInvalidPlaceholder = () => { + const { euiTheme } = useEuiTheme(); + + return ( + + + + ); +}; diff --git a/src/plugins/unified_search/public/filter_badge/filter_content/__snapshots__/filter_content.test.tsx.snap b/src/plugins/unified_search/public/filter_badge/filter_content/__snapshots__/filter_content.test.tsx.snap new file mode 100644 index 0000000000000..b71f84d22d155 --- /dev/null +++ b/src/plugins/unified_search/public/filter_badge/filter_content/__snapshots__/filter_content.test.tsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`alias 1`] = ` +
+ + geo.coordinates in US + +
+`; + +exports[`alias with error status 1`] = ` +
+ + NOT + + machine.os + : + + Error + +
+`; + +exports[`alias with warning status 1`] = ` +
+ + NOT + + machine.os + : + + Warning + +
+`; + +exports[`error 1`] = ` +
+ machine.os + : + + Error + +
+`; + +exports[`field custom label 1`] = ` +
+ test label + : + + ios + +
+`; + +exports[`warning 1`] = ` +
+ machine.os + : + + Warning + +
+`; diff --git a/src/plugins/unified_search/public/filter_bar/filter_label/filter_label.test.tsx b/src/plugins/unified_search/public/filter_badge/filter_content/filter_content.test.tsx similarity index 66% rename from src/plugins/unified_search/public/filter_bar/filter_label/filter_label.test.tsx rename to src/plugins/unified_search/public/filter_badge/filter_content/filter_content.test.tsx index 47902f3e8e047..aeb805b5113f3 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_label/filter_label.test.tsx +++ b/src/plugins/unified_search/public/filter_badge/filter_content/filter_content.test.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import FilterLabel from './filter_label'; +import FilterContent from './filter_content'; import { render } from '@testing-library/react'; import { phraseFilter } from '@kbn/data-plugin/common/stubs'; @@ -19,7 +19,7 @@ test('alias', () => { alias: 'geo.coordinates in US', }, }; - const { container } = render(); + const { container } = render(); expect(container).toMatchSnapshot(); }); @@ -28,10 +28,12 @@ test('field custom label', () => { ...phraseFilter, meta: { ...phraseFilter.meta, - alias: 'geo.coordinates in US', + alias: null, }, }; - const { container } = render(); + const { container } = render( + + ); expect(container).toMatchSnapshot(); }); @@ -40,13 +42,11 @@ test('alias with warning status', () => { ...phraseFilter, meta: { ...phraseFilter.meta, - alias: 'geo.coordinates in US', + alias: null, negate: true, }, }; - const { container } = render( - - ); + const { container } = render(); expect(container).toMatchSnapshot(); }); @@ -55,22 +55,20 @@ test('alias with error status', () => { ...phraseFilter, meta: { ...phraseFilter.meta, - alias: 'geo.coordinates in US', + alias: null, negate: true, }, }; - const { container } = render( - - ); + const { container } = render(); expect(container).toMatchSnapshot(); }); test('warning', () => { - const { container } = render(); + const { container } = render(); expect(container).toMatchSnapshot(); }); test('error', () => { - const { container } = render(); + const { container } = render(); expect(container).toMatchSnapshot(); }); diff --git a/src/plugins/unified_search/public/filter_badge/filter_content/filter_content.tsx b/src/plugins/unified_search/public/filter_badge/filter_content/filter_content.tsx new file mode 100644 index 0000000000000..21309647c67ce --- /dev/null +++ b/src/plugins/unified_search/public/filter_badge/filter_content/filter_content.tsx @@ -0,0 +1,103 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiTextColor } from '@elastic/eui'; +import type { Filter } from '@kbn/es-query'; +import { FILTERS } from '@kbn/es-query'; +import { existsOperator, isOneOfOperator } from '../../filter_bar/filter_editor'; +import { strings } from '../i18n'; + +const FilterValue = ({ value }: { value: string | number }) => { + return ( + + {` ${value}`} + + ); +}; + +const FilterField = ({ + filter, + fieldLabel, +}: { + filter: Filter; + fieldLabel?: string | undefined; +}) => { + return ( + <> + + {fieldLabel || filter.meta.key}: + + ); +}; + +const Prefix = ({ prefix }: { prefix?: boolean }) => + prefix ? {strings.getNotLabel()} : null; + +export interface FilterContentProps { + filter: Filter; + valueLabel: string; + fieldLabel?: string; + hideAlias?: boolean; +} + +export function FilterContent({ filter, valueLabel, fieldLabel, hideAlias }: FilterContentProps) { + if (!hideAlias && filter.meta.alias !== null) { + return ( + <> + + + + ); + } + + switch (filter.meta.type) { + case FILTERS.EXISTS: + return ( + <> + + + + ); + case FILTERS.PHRASES: + return ( + <> + + + + ); + case FILTERS.QUERY_STRING: + return ( + <> + + + ); + case FILTERS.PHRASE: + case FILTERS.RANGE: + return ( + <> + + + + ); + default: + return ( + <> + + + + ); + } +} + +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default FilterContent; diff --git a/src/plugins/unified_search/public/filter_badge/filter_content/index.ts b/src/plugins/unified_search/public/filter_badge/filter_content/index.ts new file mode 100644 index 0000000000000..23f2464f4a71d --- /dev/null +++ b/src/plugins/unified_search/public/filter_badge/filter_content/index.ts @@ -0,0 +1,23 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { withSuspense } from '@kbn/shared-ux-utility'; + +/** + * The Lazily-loaded `FilterContent` component. Consumers should use `React.Suspense` or + * the withSuspense` HOC to load this component. + */ +export const FilterContentLazy = React.lazy(() => import('./filter_content')); + +/** + * A `FilterContent` component that is wrapped by the `withSuspense` HOC. This component can + * be used directly by consumers and will load the `FilterContentLazy` component lazily with + * a predefined fallback and error boundary. + */ +export const FilterContent = withSuspense(FilterContentLazy); diff --git a/src/plugins/unified_search/public/filter_badge/i18n.ts b/src/plugins/unified_search/public/filter_badge/i18n.ts new file mode 100644 index 0000000000000..e4aeb472acef3 --- /dev/null +++ b/src/plugins/unified_search/public/filter_badge/i18n.ts @@ -0,0 +1,16 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const strings = { + getNotLabel: () => + i18n.translate('unifiedSearch.filter.filterBar.negatedFilterPrefix', { + defaultMessage: 'NOT ', + }), +}; diff --git a/src/plugins/unified_search/public/filter_badge/index.ts b/src/plugins/unified_search/public/filter_badge/index.ts new file mode 100644 index 0000000000000..aaa20e8b14a86 --- /dev/null +++ b/src/plugins/unified_search/public/filter_badge/index.ts @@ -0,0 +1,38 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { withSuspense } from '@kbn/shared-ux-utility'; + +export { FilterContent, FilterContentLazy } from './filter_content'; + +/** + * The Lazily-loaded `FilterBadge` component. Consumers should use `React.Suspense` or + * the withSuspense` HOC to load this component. + */ +export const FilterBadgeLazy = React.lazy(() => import('./filter_badge')); + +/** + * A `FilterBadge` component that is wrapped by the `withSuspense` HOC. This component can + * be used directly by consumers and will load the `FilterBadgeLazy` component lazily with + * a predefined fallback and error boundary. + */ +export const FilterBadge = withSuspense(FilterBadgeLazy); + +/** + * The Lazily-loaded `FilterBadgeGroup` component. Consumers should use `React.Suspense` or + * the withSuspense` HOC to load this component. + */ +export const FilterBadgeGroupLazy = React.lazy(() => import('./filter_badge_group')); + +/** + * A `FilterBadgeGroup` component that is wrapped by the `withSuspense` HOC. This component can + * be used directly by consumers and will load the `FilterBadgeGroupLazy` component lazily with + * a predefined fallback and error boundary. + */ +export const FilterBadgeGroup = withSuspense(FilterBadgeGroupLazy); diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.styles.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.styles.ts new file mode 100644 index 0000000000000..65de515c93a6e --- /dev/null +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.styles.ts @@ -0,0 +1,27 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiThemeComputed } from '@elastic/eui'; +import { css } from '@emotion/css'; + +export const filtersBuilderMaxHeightCss = (euiTheme: EuiThemeComputed) => css` + max-height: ${euiTheme.size.base} * 10; +`; + +/** @todo: should be removed, no hardcoded sizes **/ +export const filterBadgeStyle = css` + .euiFormRow__fieldWrapper { + line-height: 1.5; + } +`; + +export const filterPreviewLabelStyle = css` + & .euiFormLabel[for] { + cursor: default; + } +`; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.test.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.test.tsx index cac21f9732904..29cab95298ff6 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.test.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.test.tsx @@ -6,10 +6,11 @@ * Side Public License, v 1. */ +import React from 'react'; +import { UseEuiTheme, EuiThemeComputed } from '@elastic/eui'; import { registerTestBed, TestBed } from '@kbn/test-jest-helpers'; import type { FilterEditorProps } from '.'; import { FilterEditor } from '.'; -import React from 'react'; jest.mock('@kbn/kibana-react-plugin/public', () => { const original = jest.requireActual('@kbn/kibana-react-plugin/public'); @@ -34,6 +35,11 @@ describe('', () => { beforeEach(async () => { const defaultProps: Omit = { + theme: { + euiTheme: {} as unknown as EuiThemeComputed<{}>, + colorMode: 'DARK', + modifications: [], + } as UseEuiTheme<{}>, filter: { meta: { type: 'phase', diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.tsx index 1b336acd024ae..73bd122428077 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/filter_editor.tsx @@ -14,117 +14,173 @@ import { EuiFlexItem, EuiForm, EuiFormRow, + EuiIcon, EuiPopoverFooter, EuiPopoverTitle, EuiSpacer, - EuiSwitch, - EuiSwitchEvent, + EuiText, + EuiToolTip, + withEuiTheme, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n-react'; +import { FormattedMessage } from '@kbn/i18n-react'; import { - Filter, - FieldFilter, - buildFilter, + BooleanRelation, + buildCombinedFilter, buildCustomFilter, + buildEmptyFilter, cleanFilter, + Filter, getFilterParams, + isCombinedFilter, } from '@kbn/es-query'; -import { get } from 'lodash'; +import { merge } from 'lodash'; import React, { Component } from 'react'; +import { i18n } from '@kbn/i18n'; import { XJsonLang } from '@kbn/monaco'; -import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { DataView } from '@kbn/data-views-plugin/common'; import { getIndexPatternFromFilter } from '@kbn/data-plugin/public'; import { CodeEditor } from '@kbn/kibana-react-plugin/public'; -import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; +import { cx } from '@emotion/css'; +import { WithEuiThemeProps } from '@elastic/eui/src/services/theme'; +import { GenericComboBox } from './generic_combo_box'; import { getFieldFromFilter, - getFilterableFields, getOperatorFromFilter, - getOperatorOptions, isFilterValid, } from './lib/filter_editor_utils'; -import { Operator } from './lib/filter_operators'; -import { PhraseValueInput } from './phrase_value_input'; -import { PhrasesValuesInput } from './phrases_values_input'; -import { RangeValueInput } from './range_value_input'; -import { getFieldValidityAndErrorMessage } from './lib/helpers'; - -export interface FilterEditorProps { +import { FiltersBuilder } from '../../filters_builder'; +import { FilterBadgeGroup } from '../../filter_badge/filter_badge_group'; +import { flattenFilters } from './lib/helpers'; +import { + filterBadgeStyle, + filterPreviewLabelStyle, + filtersBuilderMaxHeightCss, +} from './filter_editor.styles'; + +export const strings = { + getPanelTitleAdd: () => + i18n.translate('unifiedSearch.filter.filterEditor.addFilterPopupTitle', { + defaultMessage: 'Add filter', + }), + getPanelTitleEdit: () => + i18n.translate('unifiedSearch.filter.filterEditor.editFilterPopupTitle', { + defaultMessage: 'Edit filter', + }), + + getAddButtonLabel: () => + i18n.translate('unifiedSearch.filter.filterEditor.addButtonLabel', { + defaultMessage: 'Add filter', + }), + getUpdateButtonLabel: () => + i18n.translate('unifiedSearch.filter.filterEditor.updateButtonLabel', { + defaultMessage: 'Update filter', + }), + getDisableToggleModeTooltip: () => + i18n.translate('unifiedSearch.filter.filterEditor.disableToggleModeTooltip', { + defaultMessage: '"Edit as Query DSL" operation is not supported for combined filters', + }), + getSelectDataViewToolTip: () => + i18n.translate('unifiedSearch.filter.filterEditor.chooseDataViewFirstToolTip', { + defaultMessage: 'You need to select a data view first', + }), + getCustomLabel: () => + i18n.translate('unifiedSearch.filter.filterEditor.createCustomLabelInputLabel', { + defaultMessage: 'Custom label (optional)', + }), + getAddCustomLabel: () => + i18n.translate('unifiedSearch.filter.filterEditor.customLabelPlaceholder', { + defaultMessage: 'Add a custom label here', + }), + getSelectDataView: () => + i18n.translate('unifiedSearch.filter.filterBar.indexPatternSelectPlaceholder', { + defaultMessage: 'Select a data view', + }), + getDataView: () => + i18n.translate('unifiedSearch.filter.filterEditor.dateViewSelectLabel', { + defaultMessage: 'Data view', + }), + getQueryDslLabel: () => + i18n.translate('unifiedSearch.filter.filterEditor.queryDslLabel', { + defaultMessage: 'Elasticsearch Query DSL', + }), + getQueryDslAriaLabel: () => + i18n.translate('unifiedSearch.filter.filterEditor.queryDslAriaLabel', { + defaultMessage: 'Elasticsearch Query DSL editor', + }), +}; +export interface FilterEditorComponentProps { filter: Filter; indexPatterns: DataView[]; onSubmit: (filter: Filter) => void; onCancel: () => void; - intl: InjectedIntl; timeRangeForSuggestionsOverride?: boolean; mode?: 'edit' | 'add'; } +export type FilterEditorProps = WithEuiThemeProps & FilterEditorComponentProps; + interface State { - selectedIndexPattern?: DataView; - selectedField?: DataViewField; - selectedOperator?: Operator; - params: any; - useCustomLabel: boolean; + selectedDataView?: DataView; customLabel: string | null; queryDsl: string; isCustomEditorOpen: boolean; + localFilter: Filter; } -const panelTitleAdd = i18n.translate('unifiedSearch.filter.filterEditor.addFilterPopupTitle', { - defaultMessage: 'Add filter', -}); -const panelTitleEdit = i18n.translate('unifiedSearch.filter.filterEditor.editFilterPopupTitle', { - defaultMessage: 'Edit filter', -}); - -const addButtonLabel = i18n.translate('unifiedSearch.filter.filterEditor.addButtonLabel', { - defaultMessage: 'Add filter', -}); -const updateButtonLabel = i18n.translate('unifiedSearch.filter.filterEditor.updateButtonLabel', { - defaultMessage: 'Update filter', -}); - -class FilterEditorUI extends Component { +class FilterEditorComponent extends Component { constructor(props: FilterEditorProps) { super(props); + const dataView = this.getIndexPatternFromFilter(); this.state = { - selectedIndexPattern: this.getIndexPatternFromFilter(), - selectedField: this.getFieldFromFilter(), - selectedOperator: this.getSelectedOperator(), - params: getFilterParams(props.filter), - useCustomLabel: props.filter.meta.alias !== null, + selectedDataView: dataView, customLabel: props.filter.meta.alias || '', - queryDsl: JSON.stringify(cleanFilter(props.filter), null, 2), + queryDsl: this.parseFilterToQueryDsl(props.filter), isCustomEditorOpen: this.isUnknownFilterType(), + localFilter: dataView ? merge({}, props.filter) : buildEmptyFilter(false), }; } + private parseFilterToQueryDsl(filter: Filter) { + return JSON.stringify(cleanFilter(filter), null, 2); + } + public render() { + const { localFilter } = this.state; + const shouldDisableToggle = isCombinedFilter(localFilter); + return (
- {this.props.mode === 'add' ? panelTitleAdd : panelTitleEdit} + + {this.props.mode === 'add' ? strings.getPanelTitleAdd() : strings.getPanelTitleEdit()} + - - {this.state.isCustomEditorOpen ? ( - - ) : ( - - )} - + + {this.state.isCustomEditorOpen ? ( + + ) : ( + + )} + + @@ -133,39 +189,19 @@ class FilterEditorUI extends Component {
{this.renderIndexPatternInput()} - {this.state.isCustomEditorOpen ? this.renderCustomEditor() : this.renderRegularEditor()} - - - - - - {this.state.useCustomLabel && ( -
- - - - -
- )} + {this.state.isCustomEditorOpen + ? this.renderCustomEditor() + : this.renderFiltersBuilderEditor()} + + + + +
@@ -183,7 +219,9 @@ class FilterEditorUI extends Component { isDisabled={!this.isFilterValid()} data-test-subj="saveFilter" > - {this.props.mode === 'add' ? addButtonLabel : updateButtonLabel} + {this.props.mode === 'add' + ? strings.getAddButtonLabel() + : strings.getUpdateButtonLabel()} @@ -220,24 +258,15 @@ class FilterEditorUI extends Component { return ''; } - const { selectedIndexPattern } = this.state; + const { selectedDataView } = this.state; return ( <> - - + indexPattern.getName()} onChange={this.onIndexPatternChange} singleSelection={{ asPlainText: true }} @@ -250,98 +279,77 @@ class FilterEditorUI extends Component { ); } - private renderRegularEditor() { - return ( -
- - {this.renderFieldInput()} - - {this.renderOperatorInput()} - - - -
{this.renderParamsEditor()}
-
- ); - } - - private renderFieldInput() { - const { selectedIndexPattern, selectedField } = this.state; - const fields = selectedIndexPattern ? getFilterableFields(selectedIndexPattern) : []; + private renderFiltersBuilderEditor() { + const { selectedDataView, localFilter } = this.state; + const flattenedFilters = flattenFilters([localFilter]); + + const shouldShowPreview = + selectedDataView && + (flattenedFilters.length > 1 || + (flattenedFilters.length === 1 && + isFilterValid( + selectedDataView, + getFieldFromFilter(flattenedFilters[0], selectedDataView), + getOperatorFromFilter(flattenedFilters[0]), + getFilterParams(flattenedFilters[0]) + ))); return ( - - field.customLabel || field.name} - onChange={this.onFieldChange} - singleSelection={{ asPlainText: true }} - isClearable={false} - data-test-subj="filterFieldSuggestionList" - /> - - ); - } + <> +
+ + + +
- private renderOperatorInput() { - const { selectedField, selectedOperator } = this.state; - const operators = selectedField ? getOperatorOptions(selectedField) : []; - return ( - - message} - onChange={this.onOperatorChange} - singleSelection={{ asPlainText: true }} - isClearable={false} - data-test-subj="filterOperatorList" - /> - + {shouldShowPreview ? ( + + , + }} + /> + + } + > + + + + + ) : null} + ); } private renderCustomEditor() { return ( - + { value={this.state.queryDsl} onChange={this.onQueryDslChange} data-test-subj="customEditorInput" - aria-label={i18n.translate('unifiedSearch.filter.filterEditor.queryDslAriaLabel', { - defaultMessage: 'Elasticsearch Query DSL editor', - })} + aria-label={strings.getQueryDslAriaLabel()} /> ); } - private renderParamsEditor() { - const indexPattern = this.state.selectedIndexPattern; - if (!indexPattern || !this.state.selectedOperator || !this.state.selectedField) { - return ''; - } - - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage( - this.state.selectedField, - this.state.params - ); - - switch (this.state.selectedOperator.type) { - case 'exists': - return ''; - case 'phrase': - return ( - - - - ); - case 'phrases': - return ( - - - - ); - case 'range': - return ( - - ); - } - } - private toggleCustomEditor = () => { const isCustomEditorOpen = !this.state.isCustomEditorOpen; this.setState({ isCustomEditorOpen }); @@ -432,31 +370,15 @@ class FilterEditorUI extends Component { private isUnknownFilterType() { const { type } = this.props.filter.meta; - return !!type && !['phrase', 'phrases', 'range', 'exists'].includes(type); + return !!type && !['phrase', 'phrases', 'range', 'exists', 'combined'].includes(type); } private getIndexPatternFromFilter() { return getIndexPatternFromFilter(this.props.filter, this.props.indexPatterns); } - private getFieldFromFilter() { - const indexPattern = this.getIndexPatternFromFilter(); - return indexPattern && getFieldFromFilter(this.props.filter as FieldFilter, indexPattern); - } - - private getSelectedOperator() { - return getOperatorFromFilter(this.props.filter); - } - private isFilterValid() { - const { - isCustomEditorOpen, - queryDsl, - selectedIndexPattern: indexPattern, - selectedField: field, - selectedOperator: operator, - params, - } = this.state; + const { isCustomEditorOpen, queryDsl, selectedDataView, localFilter } = this.state; if (isCustomEditorOpen) { try { @@ -467,35 +389,25 @@ class FilterEditorUI extends Component { } } - return isFilterValid(indexPattern, field, operator, params); - } - - private onIndexPatternChange = ([selectedIndexPattern]: DataView[]) => { - const selectedField = undefined; - const selectedOperator = undefined; - const params = undefined; - this.setState({ selectedIndexPattern, selectedField, selectedOperator, params }); - }; - - private onFieldChange = ([selectedField]: DataViewField[]) => { - const selectedOperator = undefined; - const params = undefined; - this.setState({ selectedField, selectedOperator, params }); - }; + if (!selectedDataView) { + return false; + } - private onOperatorChange = ([selectedOperator]: Operator[]) => { - // Only reset params when the operator type changes - const params = - get(this.state.selectedOperator, 'type') === get(selectedOperator, 'type') - ? this.state.params - : undefined; - this.setState({ selectedOperator, params }); - }; + return flattenFilters([localFilter]).every((f) => + isFilterValid( + selectedDataView, + getFieldFromFilter(f, selectedDataView), + getOperatorFromFilter(f), + getFilterParams(f) + ) + ); + } - private onCustomLabelSwitchChange = (event: EuiSwitchEvent) => { - const useCustomLabel = event.target.checked; - const customLabel = event.target.checked ? '' : null; - this.setState({ useCustomLabel, customLabel }); + private onIndexPatternChange = ([selectedDataView]: DataView[]) => { + this.setState({ + selectedDataView, + localFilter: buildEmptyFilter(false, selectedDataView.id), + }); }; private onCustomLabelChange = (event: React.ChangeEvent) => { @@ -503,68 +415,87 @@ class FilterEditorUI extends Component { this.setState({ customLabel }); }; - private onParamsChange = (params: any) => { - this.setState({ params }); + private onQueryDslChange = (queryDsl: string) => { + this.setState({ queryDsl }); }; - private onParamsUpdate = (value: string) => { - this.setState((prevState) => ({ params: [value, ...(prevState.params || [])] })); - }; + private onLocalFilterChange = (updatedFilters: Filter[]) => { + const { selectedDataView, customLabel } = this.state; + const alias = customLabel || null; + const { + $state, + meta: { disabled = false, negate = false }, + } = this.props.filter; - private onQueryDslChange = (queryDsl: string) => { - this.setState({ queryDsl }); + if (!$state || !$state.store || !selectedDataView) { + return; + } + + let newFilter: Filter; + + if (updatedFilters.length === 1) { + const f = updatedFilters[0]; + newFilter = { + ...f, + $state: { + store: $state.store, + }, + meta: { + ...f.meta, + disabled, + alias, + }, + }; + } else { + newFilter = buildCombinedFilter( + BooleanRelation.AND, + updatedFilters, + selectedDataView, + disabled, + negate, + alias, + $state.store + ); + } + + this.setState({ localFilter: newFilter }); }; private onSubmit = () => { + const { isCustomEditorOpen, queryDsl, customLabel } = this.state; const { - selectedIndexPattern: indexPattern, - selectedField: field, - selectedOperator: operator, - params, - useCustomLabel, - customLabel, - isCustomEditorOpen, - queryDsl, - } = this.state; - - const { $state } = this.props.filter; + $state, + meta: { index, disabled = false, negate = false }, + } = this.props.filter; + if (!$state || !$state.store) { - return; // typescript validation + return; } - const alias = useCustomLabel ? customLabel : null; if (isCustomEditorOpen) { - const { index, disabled = false, negate = false } = this.props.filter.meta; const newIndex = index || this.props.indexPatterns[0].id!; const body = JSON.parse(queryDsl); - const filter = buildCustomFilter(newIndex, body, disabled, negate, alias, $state.store); - this.props.onSubmit(filter); - } else if (indexPattern && field && operator) { - const filter = buildFilter( - indexPattern, - field, - operator.type, - operator.negate, - this.props.filter.meta.disabled ?? false, - params ?? '', - alias, + const filter = buildCustomFilter( + newIndex, + body, + disabled, + negate, + customLabel || null, $state.store ); + this.props.onSubmit(filter); + } else { + const localFilter = { + ...this.state.localFilter, + meta: { + ...this.state.localFilter.meta, + alias: customLabel || null, + }, + }; + this.props.onSubmit(localFilter); } }; } -function IndexPatternComboBox(props: GenericComboBoxProps) { - return GenericComboBox(props); -} - -function FieldComboBox(props: GenericComboBoxProps) { - return GenericComboBox(props); -} - -function OperatorComboBox(props: GenericComboBoxProps) { - return GenericComboBox(props); -} - -export const FilterEditor = injectI18n(FilterEditorUI); +export const FilterEditor = withEuiTheme(FilterEditorComponent); diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/generic_combo_box.styles.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/generic_combo_box.styles.ts new file mode 100644 index 0000000000000..fff32710eb70d --- /dev/null +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/generic_combo_box.styles.ts @@ -0,0 +1,16 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { css } from '@emotion/css'; +import type { EuiThemeComputed } from '@elastic/eui'; + +export const genericComboBoxStyle = (euiTheme: EuiThemeComputed) => css` + .euiComboBoxPlaceholder { + padding-right: calc(${euiTheme.size.xs}); + } +`; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/generic_combo_box.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/generic_combo_box.tsx index 7de0fdcf5f585..04379d34a3946 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/generic_combo_box.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/generic_combo_box.tsx @@ -6,14 +6,20 @@ * Side Public License, v 1. */ -import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption, useEuiTheme } from '@elastic/eui'; import React from 'react'; +import { genericComboBoxStyle } from './generic_combo_box.styles'; export interface GenericComboBoxProps { options: T[]; selectedOptions: T[]; getLabel: (value: T) => string; onChange: (values: T[]) => void; + renderOption?: ( + option: EuiComboBoxOptionOption, + searchValue: string, + OPTION_CONTENT_CLASSNAME: string + ) => React.ReactNode; [propName: string]: any; } @@ -25,7 +31,7 @@ export interface GenericComboBoxProps { */ export function GenericComboBox(props: GenericComboBoxProps) { const { options, selectedOptions, getLabel, onChange, ...otherProps } = props; - + const { euiTheme } = useEuiTheme(); const labels = options.map(getLabel); const euiOptions: EuiComboBoxOptionOption[] = labels.map((label) => ({ label })); const selectedEuiOptions = selectedOptions @@ -46,6 +52,7 @@ export function GenericComboBox(props: GenericComboBoxProps) { return ( field.name === filter.meta.key); +export function getFieldFromFilter(filter: Filter, indexPattern?: DataView) { + return indexPattern?.fields.find((field) => field.name === filter.meta.key); } export function getOperatorFromFilter(filter: Filter) { @@ -66,6 +66,7 @@ export function isFilterValid( if (!indexPattern || !field || !operator) { return false; } + switch (operator.type) { case 'phrase': return validateParams(params, field); diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_operators.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_operators.ts index 6143158d69d5c..5bfc6540d37d9 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_operators.ts +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_operators.ts @@ -11,6 +11,41 @@ import { FILTERS } from '@kbn/es-query'; import { ES_FIELD_TYPES } from '@kbn/field-types'; import { DataViewField } from '@kbn/data-views-plugin/common'; +export const strings = { + getIsOperatorOptionLabel: () => + i18n.translate('unifiedSearch.filter.filterEditor.isOperatorOptionLabel', { + defaultMessage: 'is', + }), + getIsNotOperatorOptionLabel: () => + i18n.translate('unifiedSearch.filter.filterEditor.isNotOperatorOptionLabel', { + defaultMessage: 'is not', + }), + getIsOneOfOperatorOptionLabel: () => + i18n.translate('unifiedSearch.filter.filterEditor.isOneOfOperatorOptionLabel', { + defaultMessage: 'is one of', + }), + getIsNotOneOfOperatorOptionLabel: () => + i18n.translate('unifiedSearch.filter.filterEditor.isNotOneOfOperatorOptionLabel', { + defaultMessage: 'is not one of', + }), + getIsBetweenOperatorOptionLabel: () => + i18n.translate('unifiedSearch.filter.filterEditor.isBetweenOperatorOptionLabel', { + defaultMessage: 'is between', + }), + getIsNotBetweenOperatorOptionLabel: () => + i18n.translate('unifiedSearch.filter.filterEditor.isNotBetweenOperatorOptionLabel', { + defaultMessage: 'is not between', + }), + getExistsOperatorOptionLabel: () => + i18n.translate('unifiedSearch.filter.filterEditor.existsOperatorOptionLabel', { + defaultMessage: 'exists', + }), + getDoesNotExistOperatorOptionLabel: () => + i18n.translate('unifiedSearch.filter.filterEditor.doesNotExistOperatorOptionLabel', { + defaultMessage: 'does not exist', + }), +}; + export interface Operator { message: string; type: FILTERS; @@ -29,43 +64,33 @@ export interface Operator { } export const isOperator = { - message: i18n.translate('unifiedSearch.filter.filterEditor.isOperatorOptionLabel', { - defaultMessage: 'is', - }), + message: strings.getIsOperatorOptionLabel(), type: FILTERS.PHRASE, negate: false, }; export const isNotOperator = { - message: i18n.translate('unifiedSearch.filter.filterEditor.isNotOperatorOptionLabel', { - defaultMessage: 'is not', - }), + message: strings.getIsNotOperatorOptionLabel(), type: FILTERS.PHRASE, negate: true, }; export const isOneOfOperator = { - message: i18n.translate('unifiedSearch.filter.filterEditor.isOneOfOperatorOptionLabel', { - defaultMessage: 'is one of', - }), + message: strings.getIsOneOfOperatorOptionLabel(), type: FILTERS.PHRASES, negate: false, fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'], }; export const isNotOneOfOperator = { - message: i18n.translate('unifiedSearch.filter.filterEditor.isNotOneOfOperatorOptionLabel', { - defaultMessage: 'is not one of', - }), + message: strings.getIsNotOneOfOperatorOptionLabel(), type: FILTERS.PHRASES, negate: true, fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'], }; export const isBetweenOperator = { - message: i18n.translate('unifiedSearch.filter.filterEditor.isBetweenOperatorOptionLabel', { - defaultMessage: 'is between', - }), + message: strings.getIsBetweenOperatorOptionLabel(), type: FILTERS.RANGE, negate: false, field: (field: DataViewField) => { @@ -79,9 +104,7 @@ export const isBetweenOperator = { }; export const isNotBetweenOperator = { - message: i18n.translate('unifiedSearch.filter.filterEditor.isNotBetweenOperatorOptionLabel', { - defaultMessage: 'is not between', - }), + message: strings.getIsNotBetweenOperatorOptionLabel(), type: FILTERS.RANGE, negate: true, field: (field: DataViewField) => { @@ -95,17 +118,13 @@ export const isNotBetweenOperator = { }; export const existsOperator = { - message: i18n.translate('unifiedSearch.filter.filterEditor.existsOperatorOptionLabel', { - defaultMessage: 'exists', - }), + message: strings.getExistsOperatorOptionLabel(), type: FILTERS.EXISTS, negate: false, }; export const doesNotExistOperator = { - message: i18n.translate('unifiedSearch.filter.filterEditor.doesNotExistOperatorOptionLabel', { - defaultMessage: 'does not exist', - }), + message: strings.getDoesNotExistOperatorOptionLabel(), type: FILTERS.EXISTS, negate: true, }; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/helpers.ts b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/helpers.ts index c0246168671f0..53c36525e1d35 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/helpers.ts +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/helpers.ts @@ -6,12 +6,20 @@ * Side Public License, v 1. */ -import type { DataViewField } from '@kbn/data-views-plugin/common'; +import { isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; -import { isEmpty } from 'lodash'; +import { Filter, isCombinedFilter } from '@kbn/es-query'; import { validateParams } from './filter_editor_utils'; +export const strings = { + getInvalidDateFormatProvidedErrorMessage: () => + i18n.translate('unifiedSearch.filter.filterBar.invalidDateFormatProvidedErrorMessage', { + defaultMessage: 'Invalid date format provided', + }), +}; + export const getFieldValidityAndErrorMessage = ( field: DataViewField, value?: string | undefined @@ -37,11 +45,21 @@ const noError = (): { isInvalid: boolean } => { const invalidFormatError = (): { isInvalid: boolean; errorMessage?: string } => { return { isInvalid: true, - errorMessage: i18n.translate( - 'unifiedSearch.filter.filterBar.invalidDateFormatProvidedErrorMessage', - { - defaultMessage: 'Invalid date format provided', - } - ), + errorMessage: strings.getInvalidDateFormatProvidedErrorMessage(), + }; +}; + +export const flattenFilters = (filter: Filter[]) => { + const returnArray: Filter[] = []; + const flattenFilterRecursively = (f: Filter) => { + if (isCombinedFilter(f)) { + f.meta.params.forEach(flattenFilterRecursively); + } else if (f) { + returnArray.push(f); + } }; + + filter.forEach(flattenFilterRecursively); + + return returnArray; }; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx index 210b201ec4c68..ff5fbc97477ca 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx @@ -10,9 +10,11 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; import { uniq } from 'lodash'; import React from 'react'; import { withKibana } from '@kbn/kibana-react-plugin/public'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; import { ValueInputType } from './value_input_type'; +import { TruncatedLabel } from './truncated_label'; interface PhraseValueInputProps extends PhraseSuggestorProps { value?: string; @@ -21,10 +23,21 @@ interface PhraseValueInputProps extends PhraseSuggestorProps { fullWidth?: boolean; compressed?: boolean; disabled?: boolean; - isInvalid?: boolean; + invalid?: boolean; } +const DEFAULT_COMBOBOX_WIDTH = 250; +const COMBOBOX_PADDINGS = 10; +const DEFAULT_FONT = '14px Inter'; + class PhraseValueInputUI extends PhraseSuggestorUI { + comboBoxRef: React.RefObject; + + constructor(props: PhraseValueInputProps) { + super(props); + this.comboBoxRef = React.createRef(); + } + public render() { return ( <> @@ -42,7 +55,7 @@ class PhraseValueInputUI extends PhraseSuggestorUI { value={this.props.value} onChange={this.props.onChange} field={this.props.field} - isInvalid={this.props.isInvalid} + isInvalid={this.props.invalid} /> )} @@ -56,24 +69,40 @@ class PhraseValueInputUI extends PhraseSuggestorUI { const valueAsStr = String(value); const options = value ? uniq([valueAsStr, ...suggestions]) : suggestions; return ( - option} - selectedOptions={value ? [valueAsStr] : []} - onChange={([newValue = '']) => onChange(newValue)} - onSearchChange={this.onSearchChange} - singleSelection={{ asPlainText: true }} - onCreateOption={onChange} - isClearable={false} - data-test-subj="filterParamsComboBox phraseParamsComboxBox" - /> +
+ option} + selectedOptions={value ? [valueAsStr] : []} + onChange={([newValue = '']) => onChange(newValue)} + onSearchChange={this.onSearchChange} + singleSelection={{ asPlainText: true }} + onCreateOption={onChange} + isClearable={false} + data-test-subj="filterParamsComboBox phraseParamsComboxBox" + renderOption={(option, searchValue) => ( + + + + + + )} + /> +
); } } diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx index 456aa47aeab40..5b3015bf232d2 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx @@ -10,43 +10,74 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; import { uniq } from 'lodash'; import React from 'react'; import { withKibana } from '@kbn/kibana-react-plugin/public'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; +import { TruncatedLabel } from './truncated_label'; -export interface PhrasesSuggestorProps extends PhraseSuggestorProps { +export interface PhrasesValuesInputProps extends PhraseSuggestorProps { values?: string[]; onChange: (values: string[]) => void; onParamsUpdate: (value: string) => void; intl: InjectedIntl; fullWidth?: boolean; compressed?: boolean; + disabled?: boolean; } -class PhrasesValuesInputUI extends PhraseSuggestorUI { +const DEFAULT_COMBOBOX_WIDTH = 250; +const COMBOBOX_PADDINGS = 20; +const DEFAULT_FONT = '14px Inter'; + +class PhrasesValuesInputUI extends PhraseSuggestorUI { + comboBoxRef: React.RefObject; + + constructor(props: PhrasesValuesInputProps) { + super(props); + this.comboBoxRef = React.createRef(); + } + public render() { const { suggestions } = this.state; - const { values, intl, onChange, fullWidth, onParamsUpdate, compressed } = this.props; + const { values, intl, onChange, fullWidth, onParamsUpdate, compressed, disabled } = this.props; const options = values ? uniq([...values, ...suggestions]) : suggestions; return ( - option} - selectedOptions={values || []} - onSearchChange={this.onSearchChange} - onCreateOption={(option: string) => { - onParamsUpdate(option.trim()); - }} - onChange={onChange} - isClearable={false} - data-test-subj="filterParamsComboBox phrasesParamsComboxBox" - /> +
+ option} + selectedOptions={values || []} + onSearchChange={this.onSearchChange} + onCreateOption={(option: string) => { + onParamsUpdate(option.trim()); + }} + onChange={onChange} + isClearable={false} + data-test-subj="filterParamsComboBox phrasesParamsComboxBox" + isDisabled={disabled} + renderOption={(option, searchValue) => ( + + + + + + )} + /> +
); } } diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx index 4ba3bf693cf54..2e371da5abfef 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/range_value_input.tsx @@ -29,6 +29,7 @@ interface Props { intl: InjectedIntl; fullWidth?: boolean; compressed?: boolean; + disabled?: boolean; } export function isRangeParams(params: any): params is RangeParams { @@ -66,6 +67,7 @@ function RangeValueInputUI(props: Props) { return (
} endControl={ @@ -99,8 +103,10 @@ function RangeValueInputUI(props: Props) { }} placeholder={props.intl.formatMessage({ id: 'unifiedSearch.filter.filterEditor.rangeEndInputPlaceholder', - defaultMessage: 'End of the range', + defaultMessage: 'End', })} + disabled={props.disabled} + dataTestSubj="range-end" /> } /> diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.test.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.test.tsx new file mode 100644 index 0000000000000..47ea4cbf2c0eb --- /dev/null +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.test.tsx @@ -0,0 +1,81 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { TruncatedLabel } from './truncated_label'; + +describe('truncated_label', () => { + const defaultProps = { + defaultFont: '14px Inter', + // jest-canvas-mock mocks measureText as the number of string characters, thats why the width is so low + width: 30, + defaultComboboxWidth: 130, + comboboxPaddings: 100, + comboBoxRef: React.createRef(), + search: '', + label: 'example_field', + }; + it('displays passed label if shorter than passed labelLength', () => { + const wrapper = mount(); + expect(wrapper.text()).toEqual('example_field'); + }); + it('middle truncates label', () => { + const wrapper = mount( + + ); + expect(wrapper.text()).toEqual('example_….subcategory.subfield'); + }); + describe('with search value passed', () => { + it('constructs truncated label when searching for the string of index = 0', () => { + const wrapper = mount( + + ); + expect(wrapper.text()).toEqual('example_space.example_field.s…'); + expect(wrapper.find('mark').text()).toEqual('example_space'); + }); + it('constructs truncated label when searching for the string in the middle', () => { + const wrapper = mount( + + ); + expect(wrapper.text()).toEqual('…ample_field.subcategory.subf…'); + expect(wrapper.find('mark').text()).toEqual('ample_field'); + }); + it('constructs truncated label when searching for the string at the end of the label', () => { + const wrapper = mount( + + ); + expect(wrapper.text()).toEqual('…le_field.subcategory.subfield'); + expect(wrapper.find('mark').text()).toEqual('subf'); + }); + + it('constructs truncated label when searching for the string longer than the truncated width and highlights the whole content', () => { + const wrapper = mount( + + ); + expect(wrapper.text()).toEqual('…ample_space.example_field.su…'); + expect(wrapper.find('mark').text()).toEqual('…ample_space.example_field.su…'); + }); + }); +}); diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.tsx new file mode 100644 index 0000000000000..9f268e46d7929 --- /dev/null +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/truncated_label.tsx @@ -0,0 +1,142 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { RefObject, useMemo } from 'react'; +import useEffectOnce from 'react-use/lib/useEffectOnce'; +import { EuiMark } from '@elastic/eui'; +import { EuiHighlight } from '@elastic/eui'; +import { throttle } from 'lodash'; + +interface TruncatedLabelProps { + label: string; + search: string; + comboBoxRef: RefObject; + defaultFont: string; + defaultComboboxWidth: number; + comboboxPaddings: number; +} + +const createContext = () => + document.createElement('canvas').getContext('2d') as CanvasRenderingContext2D; + +// extracted from getTextWidth for performance +const context = createContext(); + +const getTextWidth = (text: string, font: string) => { + const ctx = context ?? createContext(); + ctx.font = font; + const metrics = ctx.measureText(text); + return metrics.width; +}; + +const truncateLabel = ( + width: number, + font: string, + label: string, + approximateLength: number, + labelFn: (label: string, length: number) => string +) => { + let output = labelFn(label, approximateLength); + + while (getTextWidth(output, font) > width) { + approximateLength = approximateLength - 1; + const newOutput = labelFn(label, approximateLength); + if (newOutput === output) { + break; + } + output = newOutput; + } + return output; +}; + +export const TruncatedLabel = React.memo(function TruncatedLabel({ + label, + comboBoxRef, + search, + defaultFont, + defaultComboboxWidth, + comboboxPaddings, +}: TruncatedLabelProps) { + const [labelProps, setLabelProps] = React.useState<{ + width: number; + font: string; + }>({ + width: defaultComboboxWidth - comboboxPaddings, + font: defaultFont, + }); + + const computeStyles = (_e: UIEvent | undefined, shouldRecomputeAll = false) => { + if (comboBoxRef.current) { + const current = { + ...labelProps, + width: comboBoxRef.current?.clientWidth - comboboxPaddings, + }; + if (shouldRecomputeAll) { + current.font = window.getComputedStyle(comboBoxRef.current).font; + } + setLabelProps(current); + } + }; + + const handleResize = throttle((_e: UIEvent | undefined, shouldRecomputeAll = false) => { + computeStyles(_e, shouldRecomputeAll); + }, 50); + + useEffectOnce(() => { + if (comboBoxRef.current) { + handleResize(undefined, true); + } + + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + }; + }); + + const textWidth = useMemo(() => getTextWidth(label, labelProps.font), [label, labelProps.font]); + + if (textWidth < labelProps.width) { + return {label}; + } + + const searchPosition = label.indexOf(search); + const approximateLen = Math.round((labelProps.width * label.length) / textWidth); + const separator = `…`; + let separatorsLength = separator.length; + let labelFn; + + if (!search || searchPosition === -1) { + labelFn = (text: string, length: number) => + `${text.substr(0, 8)}${separator}${text.substr(text.length - (length - 8))}`; + } else if (searchPosition === 0) { + // search phrase at the beginning + labelFn = (text: string, length: number) => `${text.substr(0, length)}${separator}`; + } else if (approximateLen > label.length - searchPosition) { + // search phrase close to the end or at the end + labelFn = (text: string, length: number) => `${separator}${text.substr(text.length - length)}`; + } else { + // search phrase is in the middle + labelFn = (text: string, length: number) => + `${separator}${text.substr(searchPosition, length)}${separator}`; + separatorsLength = 2 * separator.length; + } + + const outputLabel = truncateLabel( + labelProps.width, + labelProps.font, + label, + approximateLen, + labelFn + ); + + return search.length < outputLabel.length - separatorsLength ? ( + {outputLabel} + ) : ( + {outputLabel} + ); +}); diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/value_input_type.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/value_input_type.tsx index d250f5be388d9..fc53c0e4631f7 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/value_input_type.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/value_input_type.tsx @@ -26,6 +26,7 @@ interface Props { isInvalid?: boolean; compressed?: boolean; disabled?: boolean; + dataTestSubj?: string; } class ValueInputTypeUI extends Component { @@ -38,7 +39,7 @@ class ValueInputTypeUI extends Component { }; public render() { - const value = this.props.value; + const value = this.props.value ?? ''; const type = this.props.field?.type ?? 'string'; let inputElement: React.ReactNode; switch (type) { @@ -54,6 +55,7 @@ class ValueInputTypeUI extends Component { isInvalid={!validateParams(value, this.props.field)} controlOnly={this.props.controlOnly} className={this.props.className} + data-test-subj={this.props.dataTestSubj} /> ); break; @@ -69,6 +71,7 @@ class ValueInputTypeUI extends Component { onChange={this.onChange} controlOnly={this.props.controlOnly} className={this.props.className} + data-test-subj={this.props.dataTestSubj} /> ); break; @@ -86,6 +89,7 @@ class ValueInputTypeUI extends Component { isInvalid={this.props.isInvalid} controlOnly={this.props.controlOnly} className={this.props.className} + data-test-subj={this.props.dataTestSubj} /> ); break; @@ -102,6 +106,7 @@ class ValueInputTypeUI extends Component { controlOnly={this.props.controlOnly} className={this.props.className} compressed={this.props.compressed} + data-test-subj={this.props.dataTestSubj} /> ); break; @@ -130,6 +135,7 @@ class ValueInputTypeUI extends Component { className={this.props.className} fullWidth={this.props.fullWidth} compressed={this.props.compressed} + data-test-subj={this.props.dataTestSubj} /> ); break; diff --git a/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.scss b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.scss index 94f64bdce2f65..03162ebe74d0e 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.scss +++ b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.scss @@ -71,7 +71,7 @@ } .globalFilterItem__editorForm { - padding: $euiSizeS; + padding: $euiSizeM; } .globalFilterItem__popover, diff --git a/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx index 3f70a57708b46..723a5453dd7fc 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx @@ -8,7 +8,14 @@ import './filter_item.scss'; -import { EuiContextMenu, EuiContextMenuPanel, EuiPopover, EuiPopoverProps } from '@elastic/eui'; +import { + EuiContextMenu, + EuiContextMenuPanel, + EuiPopover, + EuiPopoverProps, + euiShadowMedium, + useEuiTheme, +} from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n-react'; import { Filter, @@ -18,15 +25,11 @@ import { toggleFilterDisabled, } from '@kbn/es-query'; import classNames from 'classnames'; -import React, { MouseEvent, useState, useEffect, HTMLAttributes } from 'react'; +import React, { MouseEvent, useState, useEffect, HTMLAttributes, useMemo } from 'react'; import { IUiSettingsClient } from '@kbn/core/public'; - import { DataView } from '@kbn/data-views-plugin/public'; -import { - getIndexPatternFromFilter, - getDisplayValueFromFilter, - getFieldDisplayValueFromFilter, -} from '@kbn/data-plugin/public'; +import { css } from '@emotion/react'; +import { getIndexPatternFromFilter, getDisplayValueFromFilter } from '@kbn/data-plugin/public'; import { FilterEditor } from '../filter_editor/filter_editor'; import { FilterView } from '../filter_view'; import { FilterPanelOption } from '../../types'; @@ -62,13 +65,28 @@ export type FilterLabelStatus = | typeof FILTER_ITEM_WARNING | typeof FILTER_ITEM_ERROR; -export const FILTER_EDITOR_WIDTH = 800; +export const FILTER_EDITOR_WIDTH = 960; export function FilterItem(props: FilterItemProps) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [renderedComponent, setRenderedComponent] = useState('menu'); const { id, filter, indexPatterns, hiddenPanelOptions, readOnly = false } = props; + const euiTheme = useEuiTheme(); + + /** @todo important style should be remove after fixing elastic/eui/issues/6314. */ + const popoverDragAndDropStyle = useMemo( + () => + css` + // Always needed for popover with drag & drop in them + transform: none !important; + transition: none !important; + filter: none !important; + ${euiShadowMedium(euiTheme)} + `, + [euiTheme] + ); + useEffect(() => { if (isPopoverOpen) { setRenderedComponent('menu'); @@ -83,7 +101,7 @@ export function FilterItem(props: FilterItemProps) { } } - function handleIconClick(e: MouseEvent) { + function handleIconClick() { props.onRemove(); setIsPopoverOpen(false); } @@ -134,17 +152,19 @@ export function FilterItem(props: FilterItemProps) { function getDataTestSubj(labelConfig: LabelOptions) { const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : ''; const valueLabel = isValidLabel(labelConfig) ? labelConfig.title : labelConfig.status; - const dataTestSubjValue = valueLabel ? `filter-value-${valueLabel}` : ''; + const dataTestSubjValue = valueLabel ? `filter-value-${valueLabel.replace(/\s/g, '')}` : ''; const dataTestSubjNegated = filter.meta.negate ? 'filter-negated' : ''; const dataTestSubjDisabled = `filter-${isDisabled(labelConfig) ? 'disabled' : 'enabled'}`; const dataTestSubjPinned = `filter-${isFilterPinned(filter) ? 'pinned' : 'unpinned'}`; + const dataTestSubjId = `filter-id-${id}`; return classNames( 'filter', dataTestSubjDisabled, dataTestSubjKey, dataTestSubjValue, dataTestSubjPinned, - dataTestSubjNegated + dataTestSubjNegated, + dataTestSubjId ); } @@ -314,10 +334,10 @@ export function FilterItem(props: FilterItemProps) { filter, readOnly, valueLabel: valueLabelConfig.title, - fieldLabel: getFieldDisplayValueFromFilter(filter, indexPatterns), filterLabelStatus: valueLabelConfig.status, errorMessage: valueLabelConfig.message, className: getClasses(!!filter.meta.negate, valueLabelConfig), + dataViews: indexPatterns, iconOnClick: handleIconClick, onClick: handleBadgeClick, 'data-test-subj': getDataTestSubj(valueLabelConfig), @@ -333,6 +353,9 @@ export function FilterItem(props: FilterItemProps) { }, button: , panelPaddingSize: 'none', + panelProps: { + css: popoverDragAndDropStyle, + }, }; return readOnly ? ( @@ -344,7 +367,7 @@ export function FilterItem(props: FilterItemProps) { ) : ( +
- - geo.coordinates in US -
-`; - -exports[`alias with error status 1`] = ` -
- - NOT - - geo.coordinates in US - : - - Error - -
-`; - -exports[`alias with warning status 1`] = ` -
- - NOT - - geo.coordinates in US - : - - Warning - -
-`; - -exports[`error 1`] = ` -
- - machine.os - : - - Error - -
-`; - -exports[`field custom label 1`] = ` -
- - geo.coordinates in US -
-`; - -exports[`warning 1`] = ` -
- - machine.os - : - - Warning - -
-`; diff --git a/src/plugins/unified_search/public/filter_bar/filter_label/filter_label.tsx b/src/plugins/unified_search/public/filter_bar/filter_label/filter_label.tsx deleted file mode 100644 index 261a2a6e7afb2..0000000000000 --- a/src/plugins/unified_search/public/filter_bar/filter_label/filter_label.tsx +++ /dev/null @@ -1,97 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { Fragment } from 'react'; -import { EuiTextColor } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { Filter, FILTERS } from '@kbn/es-query'; -import type { FilterLabelStatus } from '../filter_item/filter_item'; -import { existsOperator, isOneOfOperator } from '../filter_editor'; - -export interface FilterLabelProps { - filter: Filter; - valueLabel?: string; - fieldLabel?: string; - filterLabelStatus?: FilterLabelStatus; - hideAlias?: boolean; -} - -// Needed for React.lazy -// eslint-disable-next-line import/no-default-export -export default function FilterLabel({ - filter, - valueLabel, - fieldLabel, - filterLabelStatus, - hideAlias, -}: FilterLabelProps) { - const prefixText = filter.meta.negate - ? ` ${i18n.translate('unifiedSearch.filter.filterBar.negatedFilterPrefix', { - defaultMessage: 'NOT ', - })}` - : ''; - const prefix = - filter.meta.negate && !filter.meta.disabled ? ( - {prefixText} - ) : ( - prefixText - ); - - const getValue = (text?: string) => { - return {text}; - }; - - if (!hideAlias && filter.meta.alias !== null) { - return ( - - {prefix} - {filter.meta.alias} - {filterLabelStatus && <>: {getValue(valueLabel)}} - - ); - } - - switch (filter.meta.type) { - case FILTERS.EXISTS: - return ( - - {prefix} - {fieldLabel || filter.meta.key}: {getValue(`${existsOperator.message}`)} - - ); - case FILTERS.PHRASES: - return ( - - {prefix} - {fieldLabel || filter.meta.key}: {getValue(`${isOneOfOperator.message} ${valueLabel}`)} - - ); - case FILTERS.QUERY_STRING: - return ( - - {prefix} - {getValue(`${valueLabel}`)} - - ); - case FILTERS.PHRASE: - case FILTERS.RANGE: - return ( - - {prefix} - {fieldLabel || filter.meta.key}: {getValue(valueLabel)} - - ); - default: - return ( - - {prefix} - {getValue(`${JSON.stringify(filter.query) || filter.meta.value}`)} - - ); - } -} diff --git a/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx b/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx index ffffee70534bd..1b1e7b61dcd1d 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx @@ -6,12 +6,13 @@ * Side Public License, v 1. */ -import { EuiBadge, EuiBadgeProps, EuiToolTip, useInnerText } from '@elastic/eui'; +import { EuiBadgeProps, EuiToolTip, useInnerText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { FC } from 'react'; import { Filter, isFilterPinned } from '@kbn/es-query'; -import { FilterLabel } from '..'; +import { DataView } from '@kbn/data-views-plugin/common'; import type { FilterLabelStatus } from '../filter_item/filter_item'; +import { FilterBadge } from '../../filter_badge'; interface Props { filter: Filter; @@ -22,6 +23,7 @@ interface Props { errorMessage?: string; hideAlias?: boolean; [propName: string]: any; + dataViews: DataView[]; } export const FilterView: FC = ({ @@ -34,6 +36,7 @@ export const FilterView: FC = ({ errorMessage, filterLabelStatus, hideAlias, + dataViews, ...rest }: Props) => { const [ref, innerText] = useInnerText(); @@ -92,15 +95,16 @@ export const FilterView: FC = ({ }; const FilterPill = () => ( - - - + ); return readOnly ? ( diff --git a/src/plugins/unified_search/public/filter_bar/index.tsx b/src/plugins/unified_search/public/filter_bar/index.tsx index a0fee65518fa8..d76b0f64f0ede 100644 --- a/src/plugins/unified_search/public/filter_bar/index.tsx +++ b/src/plugins/unified_search/public/filter_bar/index.tsx @@ -29,16 +29,6 @@ export const FilterItems = (props: React.ComponentProps) ); -const LazyFilterLabel = React.lazy(() => import('./filter_label/filter_label')); -/** - * Renders the label for a single filter pill - */ -export const FilterLabel = (props: React.ComponentProps) => ( - }> - - -); - const LazyFilterItem = React.lazy(() => import('./filter_item/filter_item')); /** * Renders a single filter pill diff --git a/src/plugins/unified_search/public/filters_builder/__mock__/filters.ts b/src/plugins/unified_search/public/filters_builder/__mock__/filters.ts index fae7dd7e93502..8b68d22b5123b 100644 --- a/src/plugins/unified_search/public/filters_builder/__mock__/filters.ts +++ b/src/plugins/unified_search/public/filters_builder/__mock__/filters.ts @@ -7,6 +7,7 @@ */ import type { Filter } from '@kbn/es-query'; +import { BooleanRelation } from '@kbn/es-query'; export const getFiltersMock = () => [ @@ -34,6 +35,7 @@ export const getFiltersMock = () => { meta: { type: 'combined', + relation: BooleanRelation.OR, params: [ { meta: { @@ -56,50 +58,56 @@ export const getFiltersMock = () => store: 'appState', }, }, - [ - { - meta: { - index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'category.keyword', - params: { - query: "Men's Accessories 3", - }, - }, - query: { - match_phrase: { - 'category.keyword': "Men's Accessories 3", - }, - }, - $state: { - store: 'appState', - }, - }, - { - meta: { - index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'category.keyword', - params: { - query: "Men's Accessories 4", + { + meta: { + type: 'combined', + relation: BooleanRelation.AND, + params: [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 3", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 3", + }, + }, + $state: { + store: 'appState', + }, }, - }, - query: { - match_phrase: { - 'category.keyword': "Men's Accessories 4", + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories 4", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories 4", + }, + }, + $state: { + store: 'appState', + }, }, - }, - $state: { - store: 'appState', - }, + ], }, - ], + }, { meta: { index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_context.ts b/src/plugins/unified_search/public/filters_builder/context.ts similarity index 91% rename from src/plugins/unified_search/public/filters_builder/filters_builder_context.ts rename to src/plugins/unified_search/public/filters_builder/context.ts index 8dfab23f97887..1acf317e1acad 100644 --- a/src/plugins/unified_search/public/filters_builder/filters_builder_context.ts +++ b/src/plugins/unified_search/public/filters_builder/context.ts @@ -8,7 +8,7 @@ import React, { Dispatch } from 'react'; import type { DataView } from '@kbn/data-views-plugin/common'; -import type { FiltersBuilderActions } from './filters_builder_reducer'; +import type { FiltersBuilderActions } from './reducer'; interface FiltersBuilderContextType { dataView: DataView; @@ -19,6 +19,7 @@ interface FiltersBuilderContextType { }; dropTarget: string; timeRangeForSuggestionsOverride?: boolean; + disabled: boolean; } export const FiltersBuilderContextType = React.createContext( diff --git a/src/plugins/unified_search/public/filters_builder/filter_group.styles.ts b/src/plugins/unified_search/public/filters_builder/filter_group.styles.ts new file mode 100644 index 0000000000000..9de17c5225876 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filter_group.styles.ts @@ -0,0 +1,30 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { css } from '@emotion/css'; + +export const delimiterCss = ({ + padding, + left, + background, +}: { + padding: string | null; + left: string | null; + background: string | null; +}) => css` + position: relative; + + .filter-builder__delimiter_text { + position: absolute; + display: block; + padding: 0 ${padding}; + top: 0; + left: ${left}; + background: ${background}; + } +`; diff --git a/src/plugins/unified_search/public/filters_builder/filter_group.tsx b/src/plugins/unified_search/public/filters_builder/filter_group.tsx new file mode 100644 index 0000000000000..40e0cca4f5b26 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filter_group.tsx @@ -0,0 +1,147 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useContext } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiPanel, + EuiText, + useEuiBackgroundColor, + useEuiPaddingSize, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { type Filter, BooleanRelation } from '@kbn/es-query'; +import { cx } from '@emotion/css'; +import type { Path } from './types'; +import { getBooleanRelationType } from '../utils'; +import { FilterItem } from './filter_item'; +import { FiltersBuilderContextType } from './context'; +import { getPathInArray } from './utils'; +import { delimiterCss } from './filter_group.styles'; + +export const strings = { + getDelimiterLabel: (booleanRelation: BooleanRelation) => + i18n.translate('unifiedSearch.filter.filtersBuilder.delimiterLabel', { + defaultMessage: '{booleanRelation}', + values: { + booleanRelation, + }, + }), +}; + +export interface FilterGroupProps { + filters: Filter[]; + booleanRelation: BooleanRelation; + path: Path; + + /** @internal used for recursive rendering **/ + renderedLevel?: number; + reverseBackground?: boolean; +} + +/** @internal **/ +const Delimiter = ({ + color, + booleanRelation, +}: { + color: 'subdued' | 'plain'; + booleanRelation: BooleanRelation; +}) => { + const xsPadding = useEuiPaddingSize('xs'); + const mPadding = useEuiPaddingSize('m'); + const backgroundColor = useEuiBackgroundColor(color); + return ( +
+ + + {strings.getDelimiterLabel(booleanRelation)} + +
+ ); +}; + +export const FilterGroup = ({ + filters, + booleanRelation, + path, + reverseBackground = false, + renderedLevel = 0, +}: FilterGroupProps) => { + const { + globalParams: { maxDepth, hideOr }, + } = useContext(FiltersBuilderContextType); + + const pathInArray = getPathInArray(path); + const isDepthReached = maxDepth <= pathInArray.length; + const orDisabled = hideOr || (isDepthReached && booleanRelation === BooleanRelation.AND); + const andDisabled = isDepthReached && booleanRelation === BooleanRelation.OR; + + const removeDisabled = pathInArray.length <= 1 && filters.length === 1; + const shouldNormalizeFirstLevel = + !path && filters.length === 1 && getBooleanRelationType(filters[0]); + + if (shouldNormalizeFirstLevel) { + reverseBackground = true; + renderedLevel -= 1; + } + + const color = reverseBackground ? 'plain' : 'subdued'; + + const renderedFilters = filters.map((filter, index, arrayRef) => { + const showDelimiter = booleanRelation && index + 1 < arrayRef.length; + return ( + + + + + + {showDelimiter && ( + + + + )} + + ); + }); + + return shouldNormalizeFirstLevel ? ( + <>{renderedFilters} + ) : ( + 0 ? 'none' : 'xs'} + hasBorder + className={cx({ + 'filter-builder__panel': true, + 'filter-builder__panel-nested': renderedLevel > 0, + })} + > + {renderedFilters} + + ); +}; diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/actions/action_strings.ts b/src/plugins/unified_search/public/filters_builder/filter_item/actions/action_strings.ts new file mode 100644 index 0000000000000..b1c80ba254ac2 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filter_item/actions/action_strings.ts @@ -0,0 +1,40 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const strings = { + getDeleteFilterGroupButtonIconLabel: () => + i18n.translate('unifiedSearch.filter.filtersBuilder.deleteFilterGroupButtonIcon', { + defaultMessage: 'Delete filter group', + }), + getAddOrFilterGroupButtonIconLabel: () => + i18n.translate('unifiedSearch.filter.filtersBuilder.addOrFilterGroupButtonIcon', { + defaultMessage: 'Add filter group with OR', + }), + getAddOrFilterGroupButtonLabel: () => + i18n.translate('unifiedSearch.filter.filtersBuilder.addOrFilterGroupButtonLabel', { + defaultMessage: 'OR', + }), + getAddAndFilterGroupButtonIconLabel: () => + i18n.translate('unifiedSearch.filter.filtersBuilder.addAndFilterGroupButtonIcon', { + defaultMessage: 'Add filter group with AND', + }), + getAddAndFilterGroupButtonLabel: () => + i18n.translate('unifiedSearch.filter.filtersBuilder.addAndFilterGroupButtonLabel', { + defaultMessage: 'AND', + }), + getDeleteButtonDisabled: () => + i18n.translate('unifiedSearch.filter.filtersBuilder.deleteButtonDisabled', { + defaultMessage: 'A minimum of one item is required.', + }), + getMoreActionsLabel: () => + i18n.translate('unifiedSearch.filter.filtersBuilder.moreActionsLabel', { + defaultMessage: 'More actions', + }), +}; diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/actions/actions.tsx b/src/plugins/unified_search/public/filters_builder/filter_item/actions/actions.tsx new file mode 100644 index 0000000000000..45e77bfaac6cd --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filter_item/actions/actions.tsx @@ -0,0 +1,79 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { FC } from 'react'; +import { EuiButtonEmpty, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Tooltip } from '../tooltip'; +import { strings } from './action_strings'; +import { FilterItemActionsProps } from './types'; +import { actionButtonCss } from '../filter_item.styles'; + +export const FilterItemActions: FC = ({ + disabled = false, + disableRemove = false, + hideOr = false, + disableOr = false, + hideAnd = false, + disableAnd = false, + minimizePaddings = false, + onRemoveFilter, + onOrButtonClick, + onAddButtonClick, +}) => { + return ( + + + + + + + {!hideOr && ( + + + {strings.getAddOrFilterGroupButtonLabel()} + + + )} + {!hideAnd && ( + + + {strings.getAddAndFilterGroupButtonLabel()} + + + )} + + ); +}; diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/actions/index.tsx b/src/plugins/unified_search/public/filters_builder/filter_item/actions/index.tsx new file mode 100644 index 0000000000000..6716dbfa31d92 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filter_item/actions/index.tsx @@ -0,0 +1,10 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { FilterItemActions } from './actions'; +export { MinimisedFilterItemActions } from './minimised_actions'; diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/actions/minimised_actions.tsx b/src/plugins/unified_search/public/filters_builder/filter_item/actions/minimised_actions.tsx new file mode 100644 index 0000000000000..0c9ac2a0bab18 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filter_item/actions/minimised_actions.tsx @@ -0,0 +1,38 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { FC, useState } from 'react'; +import { EuiButtonIcon, EuiPopover } from '@elastic/eui'; +import { strings } from './action_strings'; +import { FilterItemActionsProps } from './types'; +import { FilterItemActions } from './actions'; + +export const MinimisedFilterItemActions: FC = (props) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onMoreActionsButtonClick = () => { + setIsPopoverOpen((isOpen) => !isOpen); + }; + + const closePopover = () => setIsPopoverOpen(false); + + const button = ( + + ); + + return ( + + + + ); +}; diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/actions/types.ts b/src/plugins/unified_search/public/filters_builder/filter_item/actions/types.ts new file mode 100644 index 0000000000000..7d10f98320e0b --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filter_item/actions/types.ts @@ -0,0 +1,22 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface FilterItemActionsProps { + disabled?: boolean; + + disableRemove?: boolean; + onRemoveFilter: () => void; + + hideOr?: boolean; + disableOr?: boolean; + onOrButtonClick: () => void; + + hideAnd?: boolean; + disableAnd?: boolean; + onAddButtonClick: () => void; +} diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx b/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx new file mode 100644 index 0000000000000..cbfe059b09263 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx @@ -0,0 +1,111 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useContext, useRef } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FieldIcon } from '@kbn/react-field'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { + EuiFlexGroup, + EuiFlexItem, + useGeneratedHtmlId, + EuiComboBox, + EuiComboBoxOptionOption, +} from '@elastic/eui'; +import { getFilterableFields } from '../../filter_bar/filter_editor'; +import { FiltersBuilderContextType } from '../context'; +import { TruncatedLabel } from '../../filter_bar/filter_editor'; + +const DEFAULT_COMBOBOX_WIDTH = 205; +const COMBOBOX_PADDINGS = 100; +const DEFAULT_FONT = '14px Inter'; + +export const strings = { + getFieldSelectPlaceholderLabel: () => + i18n.translate('unifiedSearch.filter.filtersBuilder.fieldSelectPlaceholder', { + defaultMessage: 'Select a field', + }), +}; + +interface FieldInputProps { + dataView: DataView; + onHandleField: (field: DataViewField) => void; + field?: DataViewField; +} + +export function FieldInput({ field, dataView, onHandleField }: FieldInputProps) { + const { disabled } = useContext(FiltersBuilderContextType); + const fields = dataView ? getFilterableFields(dataView) : []; + const id = useGeneratedHtmlId({ prefix: 'fieldInput' }); + const comboBoxRef = useRef(null); + + const onFieldChange = useCallback( + ([selectedField]: DataViewField[]) => { + onHandleField(selectedField); + }, + [onHandleField] + ); + + const getLabel = useCallback( + (dataViewField: DataViewField) => ({ + label: dataViewField.customLabel || dataViewField.name, + value: dataViewField.type as KBN_FIELD_TYPES, + }), + [] + ); + + const optionFields = fields.map(getLabel); + const euiOptions: Array> = optionFields; + const selectedEuiOptions = (field ? [field] : []) + .filter((option) => fields.indexOf(option) !== -1) + .map((option) => euiOptions[fields.indexOf(option)]); + + const onComboBoxChange = (newOptions: EuiComboBoxOptionOption[]) => { + const newValues = newOptions.map( + ({ label }) => fields[optionFields.findIndex((optionField) => optionField.label === label)] + ); + onFieldChange(newValues); + }; + + return ( +
+ ( + + + + + + + + + )} + /> +
+ ); +} diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts b/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts new file mode 100644 index 0000000000000..ef9cddfd025f5 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts @@ -0,0 +1,49 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiThemeComputed } from '@elastic/eui'; +import { css } from '@emotion/css'; + +import add from '../assets/add.svg'; +import or from '../assets/or.svg'; + +export const cursorAddCss = css` + cursor: url(${add}), auto; +`; + +export const cursorOrCss = css` + cursor: url(${or}), auto; +`; + +export const fieldAndParamCss = (euiTheme: EuiThemeComputed) => css` + min-width: calc(${euiTheme.size.xl} * 5); +`; + +export const operationCss = (euiTheme: EuiThemeComputed) => css` + max-width: calc(${euiTheme.size.xl} * 4.5); + // temporary fix to be removed after https://github.com/elastic/eui/issues/2082 is fixed + .euiComboBox__inputWrap { + padding-right: calc(${euiTheme.size.base}) !important; + } +`; + +export const getGrabIconCss = (euiTheme: EuiThemeComputed) => css` + margin: 0 ${euiTheme.size.xxs}; +`; + +export const actionButtonCss = css` + &.euiButtonEmpty .euiButtonEmpty__content { + padding: 0 4px; + } +`; + +export const disabledDraggableCss = css` + &.euiDraggable .euiDraggable__item.euiDraggable__item--isDisabled { + cursor: unset; + } +`; diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.tsx b/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.tsx new file mode 100644 index 0000000000000..071a35b289c91 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.tsx @@ -0,0 +1,320 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useContext } from 'react'; +import { + EuiDraggable, + EuiDroppable, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiPanel, + useEuiTheme, + useIsWithinBreakpoints, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { Filter } from '@kbn/es-query'; +import { buildEmptyFilter, getFilterParams, BooleanRelation } from '@kbn/es-query'; +import { DataViewField } from '@kbn/data-views-plugin/common'; +import { cx } from '@emotion/css'; + +import { FieldInput } from './field_input'; +import { OperatorInput } from './operator_input'; +import { ParamsEditor } from './params_editor'; +import { getBooleanRelationType } from '../../utils'; +import { FiltersBuilderContextType } from '../context'; +import { FilterGroup } from '../filter_group'; +import type { Path } from '../types'; +import { getFieldFromFilter, getOperatorFromFilter } from '../../filter_bar/filter_editor'; +import { Operator } from '../../filter_bar/filter_editor'; +import { + cursorAddCss, + cursorOrCss, + fieldAndParamCss, + getGrabIconCss, + operationCss, + disabledDraggableCss, +} from './filter_item.styles'; +import { Tooltip } from './tooltip'; +import { FilterItemActions, MinimisedFilterItemActions } from './actions'; + +export const strings = { + getDragFilterAriaLabel: () => + i18n.translate('unifiedSearch.filter.filtersBuilder.dragFilterAriaLabel', { + defaultMessage: 'Drag filter', + }), + getReorderingRequirementsLabel: () => + i18n.translate('unifiedSearch.filter.filtersBuilder.dragHandleDisabled', { + defaultMessage: 'Reordering requires more than one item.', + }), +}; + +const MAX_FILTER_NESTING = 5; + +export interface FilterItemProps { + path: Path; + filter: Filter; + disableOr: boolean; + disableAnd: boolean; + disableRemove: boolean; + draggable?: boolean; + color: 'plain' | 'subdued'; + index: number; + + /** @internal used for recursive rendering **/ + renderedLevel: number; + reverseBackground: boolean; +} + +const isMaxFilterNesting = (path: string) => { + const pathArr = path.split('.'); + return pathArr.length - 1 === MAX_FILTER_NESTING; +}; + +export function FilterItem({ + filter, + path, + reverseBackground, + disableOr, + disableAnd, + disableRemove, + color, + index, + renderedLevel, + draggable = true, +}: FilterItemProps) { + const { + dispatch, + dataView, + dropTarget, + globalParams: { hideOr }, + timeRangeForSuggestionsOverride, + disabled, + } = useContext(FiltersBuilderContextType); + const conditionalOperationType = getBooleanRelationType(filter); + const { euiTheme } = useEuiTheme(); + let field: DataViewField | undefined; + let operator: Operator | undefined; + let params: Filter['meta']['params'] | undefined; + const isMaxNesting = isMaxFilterNesting(path); + if (!conditionalOperationType) { + field = getFieldFromFilter(filter, dataView!); + if (field) { + operator = getOperatorFromFilter(filter); + params = getFilterParams(filter); + } + } + + const onHandleField = useCallback( + (selectedField: DataViewField) => { + dispatch({ + type: 'updateFilter', + payload: { dest: { path, index }, field: selectedField }, + }); + }, + [dispatch, path, index] + ); + + const onHandleOperator = useCallback( + (selectedOperator: Operator) => { + dispatch({ + type: 'updateFilter', + payload: { dest: { path, index }, field, operator: selectedOperator }, + }); + }, + [dispatch, path, index, field] + ); + + const onHandleParamsChange = useCallback( + (selectedParams: unknown) => { + dispatch({ + type: 'updateFilter', + payload: { dest: { path, index }, field, operator, params: selectedParams }, + }); + }, + [dispatch, path, index, field, operator] + ); + + const onHandleParamsUpdate = useCallback( + (value: Filter['meta']['params']) => { + const paramsValues = Array.isArray(params) ? params : []; + dispatch({ + type: 'updateFilter', + payload: { dest: { path, index }, field, operator, params: [...paramsValues, value] }, + }); + }, + [dispatch, path, index, field, operator, params] + ); + + const onRemoveFilter = useCallback(() => { + dispatch({ + type: 'removeFilter', + payload: { + dest: { path, index }, + }, + }); + }, [dispatch, path, index]); + + const onAddFilter = useCallback( + (booleanRelation: BooleanRelation) => { + dispatch({ + type: 'addFilter', + payload: { + dest: { path, index: index + 1 }, + filter: buildEmptyFilter(false, dataView?.id), + booleanRelation, + dataView, + }, + }); + }, + [dispatch, dataView, path, index] + ); + + const onAddButtonClick = useCallback(() => onAddFilter(BooleanRelation.AND), [onAddFilter]); + const onOrButtonClick = useCallback(() => onAddFilter(BooleanRelation.OR), [onAddFilter]); + + const isMobile = useIsWithinBreakpoints(['xs', 's']); + const ActionsComponent = isMobile ? MinimisedFilterItemActions : FilterItemActions; + return ( +
0, + })} + > + {conditionalOperationType ? ( + + ) : ( + + + {(provided) => ( + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+
+ + + +
+
+
+
+ )} +
+
+ )} +
+ ); +} diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_types.ts b/src/plugins/unified_search/public/filters_builder/filter_item/index.ts similarity index 88% rename from src/plugins/unified_search/public/filters_builder/filters_builder_types.ts rename to src/plugins/unified_search/public/filters_builder/filter_item/index.ts index 24d0b9015aa74..0ed0241695890 100644 --- a/src/plugins/unified_search/public/filters_builder/filters_builder_types.ts +++ b/src/plugins/unified_search/public/filters_builder/filter_item/index.ts @@ -6,5 +6,4 @@ * Side Public License, v 1. */ -/** @internal **/ -export type Path = string; +export { FilterItem } from './filter_item'; diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_operator_input.tsx b/src/plugins/unified_search/public/filters_builder/filter_item/operator_input.tsx similarity index 74% rename from src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_operator_input.tsx rename to src/plugins/unified_search/public/filters_builder/filter_item/operator_input.tsx index 2f73df3596212..f7bcd9a910501 100644 --- a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_operator_input.tsx +++ b/src/plugins/unified_search/public/filters_builder/filter_item/operator_input.tsx @@ -6,11 +6,19 @@ * Side Public License, v 1. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useContext } from 'react'; import { i18n } from '@kbn/i18n'; import type { DataViewField } from '@kbn/data-views-plugin/common'; import type { Operator } from '../../filter_bar/filter_editor'; import { getOperatorOptions, GenericComboBox } from '../../filter_bar/filter_editor'; +import { FiltersBuilderContextType } from '../context'; + +export const strings = { + getOperatorSelectPlaceholderSelectLabel: () => + i18n.translate('unifiedSearch.filter.filtersBuilder.operatorSelectPlaceholderSelect', { + defaultMessage: 'Select operator', + }), +}; interface OperatorInputProps { field: DataViewField | undefined; @@ -25,6 +33,7 @@ export function OperatorInput({ params, onHandleOperator, }: OperatorInputProps) { + const { disabled } = useContext(FiltersBuilderContextType); const operators = field ? getOperatorOptions(field) : []; const onOperatorChange = useCallback( @@ -40,22 +49,15 @@ export function OperatorInput({ message} onChange={onOperatorChange} singleSelection={{ asPlainText: true }} isClearable={false} + data-test-subj="filterOperatorList" /> ); } diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/params_editor.tsx b/src/plugins/unified_search/public/filters_builder/filter_item/params_editor.tsx new file mode 100644 index 0000000000000..a6b1feb46c551 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filter_item/params_editor.tsx @@ -0,0 +1,73 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useContext } from 'react'; +import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { EuiToolTip, EuiFormRow } from '@elastic/eui'; +import type { Operator } from '../../filter_bar/filter_editor'; +import { getFieldValidityAndErrorMessage } from '../../filter_bar/filter_editor/lib'; +import { FiltersBuilderContextType } from '../context'; +import { ParamsEditorInput } from './params_editor_input'; + +interface ParamsEditorProps { + dataView: DataView; + params: unknown; + onHandleParamsChange: (params: unknown) => void; + onHandleParamsUpdate: (value: unknown) => void; + timeRangeForSuggestionsOverride?: boolean; + field?: DataViewField; + operator?: Operator; +} + +export function ParamsEditor({ + dataView, + field, + operator, + params, + onHandleParamsChange, + onHandleParamsUpdate, + timeRangeForSuggestionsOverride, +}: ParamsEditorProps) { + const { disabled } = useContext(FiltersBuilderContextType); + const onParamsChange = useCallback( + (selectedParams) => { + onHandleParamsChange(selectedParams); + }, + [onHandleParamsChange] + ); + + const onParamsUpdate = useCallback( + (value) => { + onHandleParamsUpdate(value); + }, + [onHandleParamsUpdate] + ); + + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage( + field!, + typeof params === 'string' ? params : undefined + ); + + return ( + + + + + + ); +} diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_params_editor.tsx b/src/plugins/unified_search/public/filters_builder/filter_item/params_editor_input.tsx similarity index 54% rename from src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_params_editor.tsx rename to src/plugins/unified_search/public/filters_builder/filter_item/params_editor_input.tsx index 17c571ac7ed39..d8230e92f9ab1 100644 --- a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_params_editor.tsx +++ b/src/plugins/unified_search/public/filters_builder/filter_item/params_editor_input.tsx @@ -6,72 +6,80 @@ * Side Public License, v 1. */ -import React, { useCallback } from 'react'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; -import { EuiFormRow } from '@elastic/eui'; -import type { Operator } from '../../filter_bar/filter_editor'; +import { EuiFieldText } from '@elastic/eui'; import { PhraseValueInput, PhrasesValuesInput, RangeValueInput, isRangeParams, } from '../../filter_bar/filter_editor'; -import { getFieldValidityAndErrorMessage } from '../../filter_bar/filter_editor/lib'; +import type { Operator } from '../../filter_bar/filter_editor'; -interface ParamsEditorProps { +export const strings = { + getSelectFieldPlaceholderLabel: () => + i18n.translate('unifiedSearch.filter.filtersBuilder.selectFieldPlaceholder', { + defaultMessage: 'Please select a field first...', + }), + getSelectOperatorPlaceholderLabel: () => + i18n.translate('unifiedSearch.filter.filtersBuilder.selectOperatorPlaceholder', { + defaultMessage: 'Please select operator first...', + }), +}; + +interface ParamsEditorInputProps { dataView: DataView; - params: TParams; - onHandleParamsChange: (params: TParams) => void; - onHandleParamsUpdate: (value: TParams) => void; + params: unknown; + onParamsChange: (params: unknown) => void; + onParamsUpdate: (value: unknown) => void; timeRangeForSuggestionsOverride?: boolean; field?: DataViewField; operator?: Operator; + invalid: boolean; + disabled: boolean; } -export function ParamsEditor({ +const getPlaceholderText = (isFieldSelected: boolean, isOperatorSelected: boolean) => { + if (!isFieldSelected) { + return strings.getSelectFieldPlaceholderLabel(); + } + + if (!isOperatorSelected) { + return strings.getSelectOperatorPlaceholderLabel(); + } + + return ''; +}; + +export function ParamsEditorInput({ dataView, field, operator, params, - onHandleParamsChange, - onHandleParamsUpdate, + invalid, + disabled, + onParamsChange, + onParamsUpdate, timeRangeForSuggestionsOverride, -}: ParamsEditorProps) { - const onParamsChange = useCallback( - (selectedParams) => { - onHandleParamsChange(selectedParams); - }, - [onHandleParamsChange] - ); - - const onParamsUpdate = useCallback( - (value) => { - onHandleParamsUpdate(value); - }, - [onHandleParamsUpdate] - ); - - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage( - field!, - typeof params === 'string' ? params : undefined - ); - +}: ParamsEditorInputProps) { switch (operator?.type) { case 'exists': return null; case 'phrase': return ( - - - + ); case 'phrases': return ( @@ -84,6 +92,7 @@ export function ParamsEditor({ onParamsUpdate={onParamsUpdate} timeRangeForSuggestionsOverride={timeRangeForSuggestionsOverride} fullWidth + disabled={disabled} /> ); case 'range': @@ -94,19 +103,18 @@ export function ParamsEditor({ value={isRangeParams(params) ? params : undefined} onChange={onParamsChange} fullWidth + disabled={disabled} /> ); + break; default: + const placeholderText = getPlaceholderText(Boolean(field), Boolean(operator?.type)); return ( - ); } diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/tooltip.tsx b/src/plugins/unified_search/public/filters_builder/filter_item/tooltip.tsx new file mode 100644 index 0000000000000..585fa3e2beb34 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filter_item/tooltip.tsx @@ -0,0 +1,27 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiToolTip, EuiToolTipProps } from '@elastic/eui'; + +export type TooltipProps = Partial> & { + content: string; + show: boolean; +}; + +export const Tooltip: React.FC = ({ children, show, content, ...tooltipProps }) => ( + <> + {show ? ( + + <>{children} + + ) : ( + children + )} + +); diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder.styles.ts b/src/plugins/unified_search/public/filters_builder/filters_builder.styles.ts new file mode 100644 index 0000000000000..39babe221788e --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/filters_builder.styles.ts @@ -0,0 +1,23 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { css } from '@emotion/css'; + +export const filtersBuilderCss = (padding: string | null) => css` + .filter-builder__panel { + &.filter-builder__panel-nested { + padding: ${padding} 0; + } + } + + .filter-builder__item { + &.filter-builder__item-nested { + padding: 0 ${padding}; + } + } +`; diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder.tsx b/src/plugins/unified_search/public/filters_builder/filters_builder.tsx index c7251bb78518c..c601cf7df0f33 100644 --- a/src/plugins/unified_search/public/filters_builder/filters_builder.tsx +++ b/src/plugins/unified_search/public/filters_builder/filters_builder.tsx @@ -6,15 +6,16 @@ * Side Public License, v 1. */ -import React, { useEffect, useReducer, useCallback, useState, useMemo } from 'react'; +import React, { useEffect, useReducer, useCallback, useState, useRef } from 'react'; import { EuiDragDropContext, DragDropContextProps, useEuiPaddingSize } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/common'; -import type { Filter } from '@kbn/es-query'; -import { css } from '@emotion/css'; -import { FiltersBuilderContextType } from './filters_builder_context'; -import { ConditionTypes } from '../utils'; -import { FilterGroup } from './filters_builder_filter_group'; -import { FiltersBuilderReducer } from './filters_builder_reducer'; +import { type Filter, BooleanRelation, compareFilters } from '@kbn/es-query'; +import { FiltersBuilderContextType } from './context'; +import { FilterGroup } from './filter_group'; +import { FiltersBuilderReducer } from './reducer'; +import { getPathInArray } from './utils'; +import { FilterLocation } from './types'; +import { filtersBuilderCss } from './filters_builder.styles'; export interface FiltersBuilderProps { filters: Filter[]; @@ -23,9 +24,10 @@ export interface FiltersBuilderProps { timeRangeForSuggestionsOverride?: boolean; maxDepth?: number; hideOr?: boolean; + disabled?: boolean; } -const rootLevelConditionType = ConditionTypes.AND; +const rootLevelConditionType = BooleanRelation.AND; const DEFAULT_MAX_DEPTH = 10; function FiltersBuilder({ @@ -35,59 +37,70 @@ function FiltersBuilder({ timeRangeForSuggestionsOverride, maxDepth = DEFAULT_MAX_DEPTH, hideOr = false, + disabled = false, }: FiltersBuilderProps) { + const filtersRef = useRef(filters); const [state, dispatch] = useReducer(FiltersBuilderReducer, { filters }); const [dropTarget, setDropTarget] = useState(''); - const mPaddingSize = useEuiPaddingSize('m'); - - const filtersBuilderStyles = useMemo( - () => css` - .filter-builder__panel { - &.filter-builder__panel-nested { - padding: ${mPaddingSize} 0; - } - } - - .filter-builder__item { - &.filter-builder__item-nested { - padding: 0 ${mPaddingSize}; - } - } - `, - [mPaddingSize] - ); + const sPaddingSize = useEuiPaddingSize('s'); + useEffect(() => { + if ( + !compareFilters(filters, filtersRef.current, { + index: true, + state: true, + negate: true, + disabled: true, + alias: true, + }) + ) { + filtersRef.current = filters; + dispatch({ type: 'updateFilters', payload: { filters } }); + } + }, [filters]); useEffect(() => { - if (state.filters !== filters) { + if (state.filters !== filtersRef.current) { + filtersRef.current = state.filters; onChange(state.filters); } - }, [filters, onChange, state.filters]); + }, [onChange, state.filters]); const handleMoveFilter = useCallback( - (pathFrom: string, pathTo: string, conditionalType: ConditionTypes) => { - if (pathFrom === pathTo) { + (from: FilterLocation, to: FilterLocation, booleanRelation: BooleanRelation) => { + if (from.path === to.path) { return null; } dispatch({ type: 'moveFilter', payload: { - pathFrom, - pathTo, - conditionalType, + from, + to, + booleanRelation, + dataView, }, }); }, - [] + [dataView] ); - const onDragEnd: DragDropContextProps['onDragEnd'] = ({ combine, source, destination }) => { + const onDragEnd: DragDropContextProps['onDragEnd'] = (args) => { + const { combine, source, destination } = args; if (source && destination) { - handleMoveFilter(source.droppableId, destination.droppableId, ConditionTypes.AND); + handleMoveFilter( + { path: source.droppableId, index: source.index }, + { path: destination.droppableId, index: destination.index }, + BooleanRelation.AND + ); } if (source && combine) { - handleMoveFilter(source.droppableId, combine.droppableId, ConditionTypes.OR); + const path = getPathInArray(combine.droppableId); + handleMoveFilter( + { path: source.droppableId, index: source.index }, + { path: combine.droppableId, index: path.at(-1) ?? 0 }, + BooleanRelation.OR + ); } setDropTarget(''); }; @@ -103,7 +116,7 @@ function FiltersBuilder({ }; return ( -
+
- +
diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_group.tsx b/src/plugins/unified_search/public/filters_builder/filters_builder_filter_group.tsx deleted file mode 100644 index adb77f7b9d2ea..0000000000000 --- a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_group.tsx +++ /dev/null @@ -1,149 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useContext, useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiPanel, - EuiText, - useEuiBackgroundColor, - useEuiPaddingSize, -} from '@elastic/eui'; -import { Filter } from '@kbn/es-query'; -import { css, cx } from '@emotion/css'; -import type { Path } from './filters_builder_types'; -import { ConditionTypes, getConditionalOperationType } from '../utils'; -import { FilterItem } from './filters_builder_filter_item'; -import { FiltersBuilderContextType } from './filters_builder_context'; -import { getPathInArray } from './filters_builder_utils'; - -export interface FilterGroupProps { - filters: Filter[]; - conditionType: ConditionTypes; - path: Path; - - /** @internal used for recursive rendering **/ - renderedLevel?: number; - reverseBackground?: boolean; -} - -/** @internal **/ -const Delimiter = ({ - color, - conditionType, -}: { - color: 'subdued' | 'plain'; - conditionType: ConditionTypes; -}) => { - const xsPadding = useEuiPaddingSize('xs'); - const mPadding = useEuiPaddingSize('m'); - const backgroundColor = useEuiBackgroundColor(color); - - const delimiterStyles = useMemo( - () => css` - position: relative; - - .filter-builder__delimiter_text { - position: absolute; - display: block; - padding: ${xsPadding}; - top: 0; - left: ${mPadding}; - background: ${backgroundColor}; - } - `, - [backgroundColor, mPadding, xsPadding] - ); - - return ( -
- - - {i18n.translate('unifiedSearch.filter.filtersBuilder.delimiterLabel', { - defaultMessage: '{conditionType}', - values: { - conditionType, - }, - })} - -
- ); -}; - -export const FilterGroup = ({ - filters, - conditionType, - path, - reverseBackground = false, - renderedLevel = 0, -}: FilterGroupProps) => { - const { - globalParams: { maxDepth, hideOr }, - } = useContext(FiltersBuilderContextType); - - const pathInArray = getPathInArray(path); - const isDepthReached = maxDepth <= pathInArray.length; - const orDisabled = hideOr || (isDepthReached && conditionType === ConditionTypes.AND); - const andDisabled = isDepthReached && conditionType === ConditionTypes.OR; - const removeDisabled = pathInArray.length <= 1 && filters.length === 1; - const shouldNormalizeFirstLevel = - !path && filters.length === 1 && getConditionalOperationType(filters[0]); - - if (shouldNormalizeFirstLevel) { - reverseBackground = true; - renderedLevel -= 1; - } - - const color = reverseBackground ? 'plain' : 'subdued'; - - const renderedFilters = filters.map((filter, index, acc) => ( - - - - - - {conditionType && index + 1 < acc.length ? ( - - {conditionType === ConditionTypes.OR && ( - - )} - - ) : null} - - )); - - return shouldNormalizeFirstLevel ? ( - <>{renderedFilters} - ) : ( - 0, - })} - > - {renderedFilters} - - ); -}; diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item.tsx b/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item.tsx deleted file mode 100644 index 30b73d397b674..0000000000000 --- a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item.tsx +++ /dev/null @@ -1,317 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useCallback, useContext, useMemo } from 'react'; -import { - EuiButtonIcon, - EuiDraggable, - EuiDroppable, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiIcon, - EuiPanel, - useEuiTheme, -} from '@elastic/eui'; -import { buildEmptyFilter, FieldFilter, Filter, getFilterParams } from '@kbn/es-query'; -import { DataViewField } from '@kbn/data-views-plugin/common'; -import { i18n } from '@kbn/i18n'; -import { cx, css } from '@emotion/css'; - -import add from '../assets/add.svg'; -import or from '../assets/or.svg'; - -import { FieldInput } from './filters_builder_filter_item_field_input'; -import { OperatorInput } from './filters_builder_filter_item_operator_input'; -import { ParamsEditor } from './filters_builder_filter_item_params_editor'; -import { ConditionTypes, getConditionalOperationType } from '../../utils'; -import { FiltersBuilderContextType } from '../filters_builder_context'; -import { FilterGroup } from '../filters_builder_filter_group'; -import type { Path } from '../filters_builder_types'; -import { getFieldFromFilter, getOperatorFromFilter } from '../../filter_bar/filter_editor'; -import { Operator } from '../../filter_bar/filter_editor'; - -export interface FilterItemProps { - path: Path; - filter: Filter; - disableOr: boolean; - disableAnd: boolean; - disableRemove: boolean; - color: 'plain' | 'subdued'; - index: number; - - /** @internal used for recursive rendering **/ - renderedLevel: number; - reverseBackground: boolean; -} - -const cursorAddStyles = css` - cursor: url(${add}), auto; -`; - -const cursorOrStyles = css` - cursor: url(${or}), auto; -`; - -export function FilterItem({ - filter, - path, - reverseBackground, - disableOr, - disableAnd, - disableRemove, - color, - index, - renderedLevel, -}: FilterItemProps) { - const { - dispatch, - dataView, - dropTarget, - globalParams: { hideOr }, - timeRangeForSuggestionsOverride, - } = useContext(FiltersBuilderContextType); - const conditionalOperationType = getConditionalOperationType(filter); - const { euiTheme } = useEuiTheme(); - - const grabIconStyles = useMemo( - () => css` - margin: 0 ${euiTheme.size.xxs}; - `, - [euiTheme.size.xxs] - ); - - let field: DataViewField | undefined; - let operator: Operator | undefined; - let params: Filter['meta']['params'] | undefined; - - if (!conditionalOperationType) { - field = getFieldFromFilter(filter as FieldFilter, dataView); - operator = getOperatorFromFilter(filter); - params = getFilterParams(filter); - } - - const onHandleField = useCallback( - (selectedField: DataViewField) => { - dispatch({ - type: 'updateFilter', - payload: { path, field: selectedField }, - }); - }, - [dispatch, path] - ); - - const onHandleOperator = useCallback( - (selectedOperator: Operator) => { - dispatch({ - type: 'updateFilter', - payload: { path, field, operator: selectedOperator }, - }); - }, - [dispatch, path, field] - ); - - const onHandleParamsChange = useCallback( - (selectedParams: string) => { - dispatch({ - type: 'updateFilter', - payload: { path, field, operator, params: selectedParams }, - }); - }, - [dispatch, path, field, operator] - ); - - const onHandleParamsUpdate = useCallback( - (value: Filter['meta']['params']) => { - dispatch({ - type: 'updateFilter', - payload: { path, params: [value, ...(params || [])] }, - }); - }, - [dispatch, path, params] - ); - - const onRemoveFilter = useCallback(() => { - dispatch({ - type: 'removeFilter', - payload: { - path, - }, - }); - }, [dispatch, path]); - - const onAddFilter = useCallback( - (conditionalType: ConditionTypes) => { - dispatch({ - type: 'addFilter', - payload: { - path, - filter: buildEmptyFilter(false, dataView.id), - conditionalType, - }, - }); - }, - [dispatch, dataView.id, path] - ); - - const onAddButtonClick = useCallback(() => onAddFilter(ConditionTypes.AND), [onAddFilter]); - const onOrButtonClick = useCallback(() => onAddFilter(ConditionTypes.OR), [onAddFilter]); - - if (!dataView) { - return null; - } - - return ( -
0, - })} - > - {conditionalOperationType ? ( - - ) : ( - - - {(provided) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {!hideOr ? ( - - - - ) : null} - - - - - - - - - - )} - - - )} -
- ); -} diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_field_input.tsx b/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_field_input.tsx deleted file mode 100644 index 3ff823a09cb5d..0000000000000 --- a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/filters_builder_filter_item_field_input.tsx +++ /dev/null @@ -1,51 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useCallback } from 'react'; -import { i18n } from '@kbn/i18n'; -import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; -import { useGeneratedHtmlId } from '@elastic/eui'; -import { getFilterableFields, GenericComboBox } from '../../filter_bar/filter_editor'; - -interface FieldInputProps { - dataView: DataView; - onHandleField: (field: DataViewField) => void; - field?: DataViewField; -} - -export function FieldInput({ field, dataView, onHandleField }: FieldInputProps) { - const fields = dataView ? getFilterableFields(dataView) : []; - const id = useGeneratedHtmlId({ prefix: 'fieldInput' }); - - const onFieldChange = useCallback( - ([selectedField]: DataViewField[]) => { - onHandleField(selectedField); - }, - [onHandleField] - ); - - const getLabel = useCallback((view: DataViewField) => view.customLabel || view.name, []); - - return ( - - ); -} diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_utils.ts b/src/plugins/unified_search/public/filters_builder/filters_builder_utils.ts deleted file mode 100644 index 4d28d091341b1..0000000000000 --- a/src/plugins/unified_search/public/filters_builder/filters_builder_utils.ts +++ /dev/null @@ -1,360 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { DataViewField } from '@kbn/data-views-plugin/common'; -import type { Filter, FilterItem } from '@kbn/es-query'; -import { cloneDeep } from 'lodash'; -import { buildCombinedFilter, isCombinedFilter } from '@kbn/es-query'; -import { ConditionTypes, getConditionalOperationType } from '../utils'; -import type { Operator } from '../filter_bar/filter_editor'; - -const PATH_SEPARATOR = '.'; - -/** - * The method returns the filter nesting identification number as an array. - * @param {string} path - variable is used to identify the filter and its nesting in the filter group. - */ -export const getPathInArray = (path: string) => path.split(PATH_SEPARATOR).map((i) => +i); - -const getGroupedFilters = (filter: FilterItem) => - Array.isArray(filter) ? filter : filter?.meta?.params; - -const doForFilterByPath = ( - filters: FilterItem[], - path: string, - action: (filter: FilterItem) => T -) => { - const pathArray = getPathInArray(path); - let f = filters[pathArray[0]]; - for (let i = 1, depth = pathArray.length; i < depth; i++) { - f = getGroupedFilters(f)[+pathArray[i]]; - } - return action(f); -}; - -const getContainerMetaByPath = (filters: FilterItem[], pathInArray: number[]) => { - let targetArray: FilterItem[] = filters; - let parentFilter: FilterItem | undefined; - let parentConditionType = ConditionTypes.AND; - - if (pathInArray.length > 1) { - parentFilter = getFilterByPath(filters, getParentFilterPath(pathInArray)); - parentConditionType = getConditionalOperationType(parentFilter) ?? parentConditionType; - targetArray = getGroupedFilters(parentFilter); - } - - return { - parentFilter, - targetArray, - parentConditionType, - }; -}; - -const getParentFilterPath = (pathInArray: number[]) => - pathInArray.slice(0, -1).join(PATH_SEPARATOR); - -/** - * The method corrects the positions of the filters after removing some filter from the filters. - * @param {FilterItem[]} filters - an array of filters that may contain filters that are incorrectly nested for later display in the UI. - */ -export const normalizeFilters = (filters: FilterItem[]) => { - const doRecursive = (f: FilterItem, parent: FilterItem) => { - if (Array.isArray(f)) { - return normalizeArray(f, parent); - } else if (isCombinedFilter(f)) { - return normalizeCombined(f); - } - return f; - }; - - const normalizeArray = (filtersArray: FilterItem[], parent: FilterItem): FilterItem[] => { - const partiallyNormalized = filtersArray - .map((item) => { - const normalized = doRecursive(item, filtersArray); - - if (Array.isArray(normalized)) { - if (normalized.length === 1) { - return normalized[0]; - } - if (normalized.length === 0) { - return undefined; - } - } - return normalized; - }, []) - .filter(Boolean) as FilterItem[]; - - return Array.isArray(parent) ? partiallyNormalized.flat() : partiallyNormalized; - }; - - const normalizeCombined = (combinedFilter: Filter): FilterItem => { - const combinedFilters = getGroupedFilters(combinedFilter); - if (combinedFilters.length < 2) { - return combinedFilters[0]; - } - - return { - ...combinedFilter, - meta: { - ...combinedFilter.meta, - params: doRecursive(combinedFilters, combinedFilter), - }, - }; - }; - - return normalizeArray(filters, filters) as Filter[]; -}; - -/** - * Find filter by path. - * @param {FilterItem[]} filters - filters in which the search for the desired filter will occur. - * @param {string} path - path to filter. - */ -export const getFilterByPath = (filters: FilterItem[], path: string) => - doForFilterByPath(filters, path, (f) => f); - -/** - * Method to add a filter to a specified location in a filter group. - * @param {Filter[]} filters - array of filters where the new filter will be added. - * @param {FilterItem} filter - new filter. - * @param {string} path - path to filter. - * @param {ConditionTypes} conditionalType - OR/AND relationships between filters. - */ -export const addFilter = ( - filters: Filter[], - filter: FilterItem, - path: string, - conditionalType: ConditionTypes -) => { - const newFilters = cloneDeep(filters); - const pathInArray = getPathInArray(path); - const { targetArray, parentConditionType } = getContainerMetaByPath(newFilters, pathInArray); - const selector = pathInArray[pathInArray.length - 1]; - - if (parentConditionType !== conditionalType) { - if (conditionalType === ConditionTypes.OR) { - targetArray.splice(selector, 1, buildCombinedFilter([targetArray[selector], filter])); - } - if (conditionalType === ConditionTypes.AND) { - targetArray.splice(selector, 1, [targetArray[selector], filter]); - } - } else { - targetArray.splice(selector + 1, 0, filter); - } - - return newFilters; -}; - -/** - * Remove filter from specified location. - * @param {Filter[]} filters - array of filters. - * @param {string} path - path to filter. - */ -export const removeFilter = (filters: Filter[], path: string) => { - const newFilters = cloneDeep(filters); - const pathInArray = getPathInArray(path); - const { targetArray } = getContainerMetaByPath(newFilters, pathInArray); - const selector = pathInArray[pathInArray.length - 1]; - - targetArray.splice(selector, 1); - - return normalizeFilters(newFilters); -}; - -/** - * Moving the filter on drag and drop. - * @param {Filter[]} filters - array of filters. - * @param {string} from - filter path before moving. - * @param {string} to - filter path where the filter will be moved. - * @param {ConditionTypes} conditionalType - OR/AND relationships between filters. - */ -export const moveFilter = ( - filters: Filter[], - from: string, - to: string, - conditionalType: ConditionTypes -) => { - const addFilterThenRemoveFilter = ( - source: Filter[], - addedFilter: FilterItem, - pathFrom: string, - pathTo: string, - conditional: ConditionTypes - ) => { - const newFiltersWithFilter = addFilter(source, addedFilter, pathTo, conditional); - return removeFilter(newFiltersWithFilter, pathFrom); - }; - - const removeFilterThenAddFilter = ( - source: Filter[], - removableFilter: FilterItem, - pathFrom: string, - pathTo: string, - conditional: ConditionTypes - ) => { - const newFiltersWithoutFilter = removeFilter(source, pathFrom); - return addFilter(newFiltersWithoutFilter, removableFilter, pathTo, conditional); - }; - - const newFilters = cloneDeep(filters); - const movingFilter = getFilterByPath(newFilters, from); - - const pathInArrayTo = getPathInArray(to); - const pathInArrayFrom = getPathInArray(from); - - if (pathInArrayTo.length === pathInArrayFrom.length) { - const filterPositionTo = pathInArrayTo.at(-1); - const filterPositionFrom = pathInArrayFrom.at(-1); - - const { parentConditionType } = getContainerMetaByPath(newFilters, pathInArrayTo); - const filterMovementDirection = Number(filterPositionTo) - Number(filterPositionFrom); - - if (filterMovementDirection === -1 && parentConditionType === conditionalType) { - return filters; - } - - if (filterMovementDirection >= -1) { - return addFilterThenRemoveFilter(newFilters, movingFilter, from, to, conditionalType); - } else { - return removeFilterThenAddFilter(newFilters, movingFilter, from, to, conditionalType); - } - } - - if (pathInArrayTo.length > pathInArrayFrom.length) { - return addFilterThenRemoveFilter(newFilters, movingFilter, from, to, conditionalType); - } else { - return removeFilterThenAddFilter(newFilters, movingFilter, from, to, conditionalType); - } -}; - -/** - * Method to update values inside filter. - * @param {Filter[]} filters - filter array - * @param {string} path - path to filter - * @param {DataViewField} field - DataViewField property inside a filter - * @param {Operator} operator - defines a relation by property and value - * @param {Filter['meta']['params']} params - filter value - */ -export const updateFilter = ( - filters: Filter[], - path: string, - field?: DataViewField, - operator?: Operator, - params?: Filter['meta']['params'] -) => { - const newFilters = [...filters]; - const changedFilter = getFilterByPath(newFilters, path) as Filter; - let filter = Object.assign({}, changedFilter); - - if (field && operator && params) { - if (Array.isArray(params)) { - filter = updateWithIsOneOfOperator(filter, operator, params); - } else { - filter = updateWithIsOperator(filter, operator, params); - } - } else if (field && operator) { - if (operator.type === 'exists') { - filter = updateWithExistsOperator(filter, operator); - } else { - filter = updateOperator(filter, operator); - } - } else { - filter = updateField(filter, field); - } - - const pathInArray = getPathInArray(path); - const { targetArray } = getContainerMetaByPath(newFilters, pathInArray); - const selector = pathInArray[pathInArray.length - 1]; - targetArray.splice(selector, 1, filter); - - return newFilters; -}; - -function updateField(filter: Filter, field?: DataViewField) { - return { - ...filter, - meta: { - ...filter.meta, - key: field?.name, - params: { query: undefined }, - value: undefined, - type: undefined, - }, - query: undefined, - }; -} - -function updateOperator(filter: Filter, operator?: Operator) { - return { - ...filter, - meta: { - ...filter.meta, - negate: operator?.negate, - type: operator?.type, - params: { ...filter.meta.params, query: undefined }, - value: undefined, - }, - query: { match_phrase: { field: filter.meta.key } }, - }; -} - -function updateWithExistsOperator(filter: Filter, operator?: Operator) { - return { - ...filter, - meta: { - ...filter.meta, - negate: operator?.negate, - type: operator?.type, - params: undefined, - value: 'exists', - }, - query: { exists: { field: filter.meta.key } }, - }; -} - -function updateWithIsOperator( - filter: Filter, - operator?: Operator, - params?: Filter['meta']['params'] -) { - return { - ...filter, - meta: { - ...filter.meta, - negate: operator?.negate, - type: operator?.type, - params: { ...filter.meta.params, query: params }, - }, - query: { match_phrase: { ...filter!.query?.match_phrase, [filter.meta.key!]: params } }, - }; -} - -function updateWithIsOneOfOperator( - filter: Filter, - operator?: Operator, - params?: Array -) { - return { - ...filter, - meta: { - ...filter.meta, - negate: operator?.negate, - type: operator?.type, - params, - }, - query: { - bool: { - minimum_should_match: 1, - ...filter!.query?.should, - should: params?.map((param) => { - return { match_phrase: { [filter.meta.key!]: param } }; - }), - }, - }, - }; -} diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_reducer.ts b/src/plugins/unified_search/public/filters_builder/reducer.ts similarity index 62% rename from src/plugins/unified_search/public/filters_builder/filters_builder_reducer.ts rename to src/plugins/unified_search/public/filters_builder/reducer.ts index 3dde3bdddac67..5f8ce1aa8188e 100644 --- a/src/plugins/unified_search/public/filters_builder/filters_builder_reducer.ts +++ b/src/plugins/unified_search/public/filters_builder/reducer.ts @@ -7,28 +7,33 @@ */ import type { Reducer } from 'react'; -import type { Filter } from '@kbn/es-query'; -import type { DataViewField } from '@kbn/data-views-plugin/common'; -import type { Path } from './filters_builder_types'; -import type { ConditionTypes } from '../utils'; -import { addFilter, moveFilter, removeFilter, updateFilter } from './filters_builder_utils'; +import type { Filter, BooleanRelation } from '@kbn/es-query'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { addFilter, moveFilter, removeFilter, updateFilters } from './utils'; import type { Operator } from '../filter_bar/filter_editor'; +import { FilterLocation } from './types'; /** @internal **/ export interface FiltersBuilderState { filters: Filter[]; } +/** @internal **/ +export interface UpdateFiltersPayload { + filters: Filter[]; +} + /** @internal **/ export interface AddFilterPayload { - path: Path; + dest: FilterLocation; filter: Filter; - conditionalType: ConditionTypes; + booleanRelation: BooleanRelation; + dataView: DataView; } /** @internal **/ export interface UpdateFilterPayload { - path: string; + dest: FilterLocation; field?: DataViewField; operator?: Operator; params?: Filter['meta']['params']; @@ -36,18 +41,20 @@ export interface UpdateFilterPayload { /** @internal **/ export interface RemoveFilterPayload { - path: Path; + dest: FilterLocation; } /** @internal **/ export interface MoveFilterPayload { - pathFrom: Path; - pathTo: Path; - conditionalType: ConditionTypes; + from: FilterLocation; + to: FilterLocation; + booleanRelation: BooleanRelation; + dataView: DataView; } /** @internal **/ export type FiltersBuilderActions = + | { type: 'updateFilters'; payload: UpdateFiltersPayload } | { type: 'addFilter'; payload: AddFilterPayload } | { type: 'removeFilter'; payload: RemoveFilterPayload } | { type: 'moveFilter'; payload: MoveFilterPayload } @@ -58,36 +65,44 @@ export const FiltersBuilderReducer: Reducer { switch (action.type) { + case 'updateFilters': + return { + ...state, + filters: action.payload.filters, + }; case 'addFilter': return { + ...state, filters: addFilter( state.filters, action.payload.filter, - action.payload.path, - action.payload.conditionalType + action.payload.dest, + action.payload.booleanRelation, + action.payload.dataView ), }; case 'removeFilter': return { ...state, - filters: removeFilter(state.filters, action.payload.path), + filters: removeFilter(state.filters, action.payload.dest), }; case 'moveFilter': return { ...state, filters: moveFilter( state.filters, - action.payload.pathFrom, - action.payload.pathTo, - action.payload.conditionalType + action.payload.from, + action.payload.to, + action.payload.booleanRelation, + action.payload.dataView ), }; case 'updateFilter': return { ...state, - filters: updateFilter( + filters: updateFilters( state.filters, - action.payload.path, + action.payload.dest, action.payload.field, action.payload.operator, action.payload.params diff --git a/src/plugins/unified_search/public/filters_builder/types.ts b/src/plugins/unified_search/public/filters_builder/types.ts new file mode 100644 index 0000000000000..0077cd00d4942 --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/types.ts @@ -0,0 +1,16 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** @internal **/ +export type Path = string; + +/** @internal **/ +export interface FilterLocation { + path: Path; + index: number; +} diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_utils.test.ts b/src/plugins/unified_search/public/filters_builder/utils/filters_builder.test.ts similarity index 68% rename from src/plugins/unified_search/public/filters_builder/filters_builder_utils.test.ts rename to src/plugins/unified_search/public/filters_builder/utils/filters_builder.test.ts index 56a0eb97b80b2..534237b0ff7bc 100644 --- a/src/plugins/unified_search/public/filters_builder/filters_builder_utils.test.ts +++ b/src/plugins/unified_search/public/filters_builder/utils/filters_builder.test.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { buildEmptyFilter, Filter, FilterItem } from '@kbn/es-query'; -import { ConditionTypes } from '../utils'; +import { buildEmptyFilter, type Filter, BooleanRelation } from '@kbn/es-query'; +import { DataView } from '@kbn/data-views-plugin/common'; import { getFilterByPath, getPathInArray, @@ -15,17 +15,17 @@ import { removeFilter, moveFilter, normalizeFilters, -} from './filters_builder_utils'; -import { getConditionalOperationType } from '../utils'; +} from './filters_builder'; +import { getBooleanRelationType } from '../../utils'; import { getDataAfterNormalized, getDataThatNeedNotNormalized, getDataThatNeedsNormalized, getFiltersMock, -} from './__mock__/filters'; +} from '../__mock__/filters'; -describe('filters_builder_utils', () => { +describe('filters_builder', () => { let filters: Filter[]; beforeAll(() => { filters = getFiltersMock(); @@ -126,69 +126,75 @@ describe('filters_builder_utils', () => { } `); expect(getFilterByPath(filters, '1.1')).toMatchInlineSnapshot(` - Array [ - Object { - "$state": Object { - "store": "appState", - }, - "meta": Object { - "alias": null, - "disabled": false, - "index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", - "key": "category.keyword", - "negate": false, - "params": Object { - "query": "Men's Accessories 3", - }, - "type": "phrase", - }, - "query": Object { - "match_phrase": Object { - "category.keyword": "Men's Accessories 3", - }, - }, - }, - Object { - "$state": Object { - "store": "appState", - }, - "meta": Object { - "alias": null, - "disabled": false, - "index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", - "key": "category.keyword", - "negate": false, - "params": Object { - "query": "Men's Accessories 4", + Object { + "meta": Object { + "params": Array [ + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "key": "category.keyword", + "negate": false, + "params": Object { + "query": "Men's Accessories 3", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "category.keyword": "Men's Accessories 3", + }, + }, }, - "type": "phrase", - }, - "query": Object { - "match_phrase": Object { - "category.keyword": "Men's Accessories 4", + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "key": "category.keyword", + "negate": false, + "params": Object { + "query": "Men's Accessories 4", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "category.keyword": "Men's Accessories 4", + }, + }, }, - }, + ], + "relation": "AND", + "type": "combined", }, - ] + } `); }); }); - describe('getConditionalOperationType', () => { + describe('getBooleanRelationType', () => { let filter: Filter; - let filtersWithOrRelationships: FilterItem; - let groupOfFilters: FilterItem; + let filtersWithOrRelationships: Filter; + let groupOfFilters: Filter; beforeAll(() => { filter = filters[0]; filtersWithOrRelationships = filters[1]; - groupOfFilters = filters[1].meta.params; + groupOfFilters = filters[1].meta.params[1]; }); test('should return correct ConditionalOperationType', () => { - expect(getConditionalOperationType(filter)).toBeUndefined(); - expect(getConditionalOperationType(filtersWithOrRelationships)).toBe(ConditionTypes.OR); - expect(getConditionalOperationType(groupOfFilters)).toBe(ConditionTypes.AND); + expect(getBooleanRelationType(filter)).toBeUndefined(); + expect(getBooleanRelationType(filtersWithOrRelationships)).toBe(BooleanRelation.OR); + expect(getBooleanRelationType(groupOfFilters)).toBe(BooleanRelation.AND); }); }); @@ -204,7 +210,15 @@ describe('filters_builder_utils', () => { const emptyFilter = buildEmptyFilter(false); test('should add filter into filters after zero element', () => { - const enlargedFilters = addFilter(filters, emptyFilter, '0', ConditionTypes.AND); + const enlargedFilters = addFilter( + filters, + emptyFilter, + { index: 1, path: '0' }, + BooleanRelation.AND, + { + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + } as DataView + ); expect(getFilterByPath(enlargedFilters, '1')).toMatchInlineSnapshot(` Object { "$state": Object { @@ -225,7 +239,7 @@ describe('filters_builder_utils', () => { test('should remove filter from filters', () => { const path = '1.1'; const filterBeforeRemoved = getFilterByPath(filters, path); - const filtersAfterRemoveFilter = removeFilter(filters, path); + const filtersAfterRemoveFilter = removeFilter(filters, { index: 1, path }); const filterObtainedAfterFilterRemovalFromFilters = getFilterByPath( filtersAfterRemoveFilter, path @@ -238,7 +252,15 @@ describe('filters_builder_utils', () => { describe('moveFilter', () => { test('should move filter from "0" path to "2" path into filters', () => { const filterBeforeMoving = getFilterByPath(filters, '0'); - const filtersAfterMovingFilter = moveFilter(filters, '0', '2', ConditionTypes.AND); + const filtersAfterMovingFilter = moveFilter( + filters, + { path: '0', index: 1 }, + { path: '2', index: 3 }, + BooleanRelation.AND, + { + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + } as DataView + ); const filterObtainedAfterFilterMovingFilters = getFilterByPath(filtersAfterMovingFilter, '2'); expect(filterBeforeMoving).toEqual(filterObtainedAfterFilterMovingFilters); }); diff --git a/src/plugins/unified_search/public/filters_builder/utils/filters_builder.ts b/src/plugins/unified_search/public/filters_builder/utils/filters_builder.ts new file mode 100644 index 0000000000000..182fa65ba971b --- /dev/null +++ b/src/plugins/unified_search/public/filters_builder/utils/filters_builder.ts @@ -0,0 +1,194 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { Filter, updateFilter } from '@kbn/es-query'; +import { BooleanRelation } from '@kbn/es-query'; +import { cloneDeep } from 'lodash'; +import { buildCombinedFilter, isCombinedFilter } from '@kbn/es-query'; +import { getBooleanRelationType } from '../../utils'; +import type { Operator } from '../../filter_bar/filter_editor'; +import { FilterLocation, Path } from '../types'; + +const PATH_SEPARATOR = '.'; + +export const getPathInArray = (path: Path) => path.split(PATH_SEPARATOR).map(Number); + +const getGroupedFilters = (filter: Filter): Filter[] => + Array.isArray(filter) ? filter : filter?.meta?.params ?? []; + +const doForFilterByPath = (filters: Filter[], path: Path, action: (filter: Filter) => T) => { + const [first, ...restPath] = getPathInArray(path); + + const foundFilter = restPath.reduce((filter, filterLocation) => { + return getGroupedFilters(filter)[Number(filterLocation)]; + }, filters[first]); + + return action(foundFilter); +}; + +const getContainerMetaByPath = (filters: Filter[], pathInArray: number[]) => { + if (pathInArray.length <= 1) { + return { + parentFilter: undefined, + targetArray: filters, + parentConditionType: BooleanRelation.AND, + }; + } + + const parentFilter = getFilterByPath(filters, getParentFilterPath(pathInArray)); + const targetArray = getGroupedFilters(parentFilter); + + return { + parentFilter, + targetArray: Array.isArray(targetArray) ? targetArray : targetArray ? [targetArray] : [], + parentConditionType: getBooleanRelationType(parentFilter) ?? BooleanRelation.AND, + }; +}; + +const getParentFilterPath = (pathInArray: number[]) => + pathInArray.slice(0, -1).join(PATH_SEPARATOR); + +export const normalizeFilters = (filters: Filter[]) => { + const normalizeRecursively = ( + f: Filter | Filter[], + parent: Filter[] | Filter + ): Filter | Filter[] | undefined => { + if (Array.isArray(f)) { + return normalizeArray(f, parent); + } else if (isCombinedFilter(f)) { + return normalizeCombined(f); + } + return f; + }; + + const normalizeArray = (filtersArray: Filter[], parent: Filter[] | Filter): Filter[] => { + const partiallyNormalized = filtersArray + .map((item: Filter) => { + const normalized = normalizeRecursively(item, filtersArray); + + if (Array.isArray(normalized)) { + if (normalized.length === 1) { + return normalized[0]; + } + if (normalized.length === 0) { + return undefined; + } + } + return normalized; + }, []) + .filter(Boolean) as Filter[]; + return Array.isArray(parent) ? partiallyNormalized.flat() : partiallyNormalized; + }; + + const normalizeCombined = (combinedFilter: Filter) => { + const combinedFilters = getGroupedFilters(combinedFilter); + const nonEmptyCombinedFilters = combinedFilters.filter(Boolean); + if (nonEmptyCombinedFilters.length < 2) { + return nonEmptyCombinedFilters[0]; + } + + return combinedFilter + ? { + ...combinedFilter, + meta: { + ...combinedFilter.meta, + params: normalizeRecursively(nonEmptyCombinedFilters, combinedFilter), + }, + } + : undefined; + }; + + return normalizeArray(filters, filters) as Filter[]; +}; + +export const getFilterByPath = (filters: Filter[], path: Path) => + doForFilterByPath(filters, path, (f) => f); + +export const addFilter = ( + filters: Filter[], + filter: Filter, + dest: FilterLocation, + booleanRelation: BooleanRelation, + dataView: DataView +) => { + const newFilters = cloneDeep(filters); + const pathInArray = getPathInArray(dest.path); + const { targetArray, parentConditionType } = getContainerMetaByPath(newFilters, pathInArray); + const selector = pathInArray.at(-1) ?? 0; + + if (booleanRelation && parentConditionType !== booleanRelation) { + targetArray.splice( + selector, + 1, + buildCombinedFilter(booleanRelation, [targetArray[selector], filter], dataView) + ); + } else { + targetArray.splice(dest.index, 0, filter); + } + return newFilters; +}; + +const removeFilterWithoutNormalization = (filters: Filter[], dest: FilterLocation) => { + const newFilters = cloneDeep(filters); + const pathInArray = getPathInArray(dest.path); + const meta = getContainerMetaByPath(newFilters, pathInArray); + const target: Array = meta.targetArray; + target[dest.index] = undefined; + + return newFilters; +}; + +export const removeFilter = (filters: Filter[], dest: FilterLocation) => { + const newFilters = removeFilterWithoutNormalization(filters, dest); + return normalizeFilters(newFilters); +}; + +export const moveFilter = ( + filters: Filter[], + from: FilterLocation, + to: FilterLocation, + booleanRelation: BooleanRelation, + dataView: DataView +) => { + const newFilters = cloneDeep(filters); + const movingFilter = getFilterByPath(newFilters, from.path); + const filtersWithoutRemoved = removeFilterWithoutNormalization(newFilters, from); + + const updatedFilters = addFilter( + filtersWithoutRemoved, + movingFilter, + to, + booleanRelation, + dataView + ); + + return normalizeFilters(updatedFilters); +}; + +export const updateFilters = ( + filters: Filter[], + dest: FilterLocation, + field?: DataViewField, + operator?: Operator, + params?: Filter['meta']['params'] +) => { + const newFilters = [...filters]; + const updatedFilter = updateFilter( + getFilterByPath(newFilters, dest.path), + field?.name, + operator, + params + ); + const pathInArray = getPathInArray(dest.path); + const { targetArray } = getContainerMetaByPath(newFilters, pathInArray); + const selector = pathInArray[pathInArray.length - 1]; + targetArray.splice(selector, 1, updatedFilter); + + return newFilters; +}; diff --git a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/index.ts b/src/plugins/unified_search/public/filters_builder/utils/index.ts similarity index 85% rename from src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/index.ts rename to src/plugins/unified_search/public/filters_builder/utils/index.ts index 07dd57964a13e..6d84cc7bcca25 100644 --- a/src/plugins/unified_search/public/filters_builder/filters_builder_filter_item/index.ts +++ b/src/plugins/unified_search/public/filters_builder/utils/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { FilterItem } from './filters_builder_filter_item'; +export * from './filters_builder'; diff --git a/src/plugins/unified_search/public/index.ts b/src/plugins/unified_search/public/index.ts index afaaa537d6cf2..3f0e26c5aba63 100755 --- a/src/plugins/unified_search/public/index.ts +++ b/src/plugins/unified_search/public/index.ts @@ -19,7 +19,8 @@ export type { } from './types'; export { SearchBar } from './search_bar'; export type { FilterItemsProps } from './filter_bar'; -export { FilterLabel, FilterItem, FilterItems } from './filter_bar'; +export { FilterItem, FilterItems } from './filter_bar'; +export { FilterBadgeGroup } from './filter_badge'; export { DataViewsList } from './dataview_picker/dataview_list'; export { DataViewSelector } from './dataview_picker/data_view_selector'; export { DataViewPicker } from './dataview_picker'; diff --git a/src/plugins/unified_search/public/query_string_input/add_filter_popover.styles.ts b/src/plugins/unified_search/public/query_string_input/add_filter_popover.styles.ts new file mode 100644 index 0000000000000..21e3d6b649175 --- /dev/null +++ b/src/plugins/unified_search/public/query_string_input/add_filter_popover.styles.ts @@ -0,0 +1,20 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { euiShadowMedium, UseEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; + +/** @todo important style should be remove after fixing elastic/eui/issues/6314. */ +export const popoverDragAndDropCss = (euiTheme: UseEuiTheme) => + css` + // Always needed for popover with drag & drop in them + transform: none !important; + transition: none !important; + filter: none !important; + ${euiShadowMedium(euiTheme)} + `; diff --git a/src/plugins/unified_search/public/query_string_input/add_filter_popover.tsx b/src/plugins/unified_search/public/query_string_input/add_filter_popover.tsx index 997a5f6c66a3b..186b9a9874dd7 100644 --- a/src/plugins/unified_search/public/query_string_input/add_filter_popover.tsx +++ b/src/plugins/unified_search/public/query_string_input/add_filter_popover.tsx @@ -7,17 +7,26 @@ */ import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiFlexItem, EuiButtonIcon, EuiPopover, EuiButtonIconProps, EuiToolTip, + useEuiTheme, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { Filter } from '@kbn/es-query'; import type { DataView } from '@kbn/data-views-plugin/public'; import { FilterEditorWrapper } from './filter_editor_wrapper'; +import { popoverDragAndDropCss } from './add_filter_popover.styles'; + +export const strings = { + getAddFilterButtonLabel: () => + i18n.translate('unifiedSearch.filter.filterBar.addFilterButtonLabel', { + defaultMessage: 'Add filter', + }), +}; interface AddFilterPopoverProps { indexPatterns?: Array; @@ -36,18 +45,15 @@ export const AddFilterPopover = React.memo(function AddFilterPopover({ buttonProps, isDisabled, }: AddFilterPopoverProps) { + const euiTheme = useEuiTheme(); const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false); - const buttonIconLabel = i18n.translate('unifiedSearch.filter.filterBar.addFilterButtonLabel', { - defaultMessage: 'Add filter', - }); - const button = ( - + setIsAddFilterPopoverOpen((isOpen) => !isOpen)} size="m" @@ -67,7 +73,10 @@ export const AddFilterPopover = React.memo(function AddFilterPopover({ closePopover={() => setIsAddFilterPopoverOpen(false)} anchorPosition="downLeft" panelPaddingSize="none" - panelProps={{ 'data-test-subj': 'addFilterPopover' }} + panelProps={{ + 'data-test-subj': 'addFilterPopover', + css: popoverDragAndDropCss(euiTheme), + }} initialFocus=".filterEditor__hiddenItem" ownFocus repositionOnScroll diff --git a/src/plugins/unified_search/public/query_string_input/language_switcher.tsx b/src/plugins/unified_search/public/query_string_input/language_switcher.tsx index 0c369b4efc077..7d69bd0c7b6e5 100644 --- a/src/plugins/unified_search/public/query_string_input/language_switcher.tsx +++ b/src/plugins/unified_search/public/query_string_input/language_switcher.tsx @@ -16,9 +16,16 @@ import { EuiButtonIcon, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { DocLinksStart } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; + +export const strings = { + getSwitchLanguageButtonText: () => + i18n.translate('unifiedSearch.switchLanguage.buttonText', { + defaultMessage: 'Switch language button.', + }), +}; export interface QueryLanguageSwitcherProps { language: string; @@ -51,9 +58,7 @@ export const QueryLanguageSwitcher = React.memo(function QueryLanguageSwitcher({ onClick={() => setIsPopoverOpen(!isPopoverOpen)} className="euiFormControlLayout__append kqlQueryBar__languageSwitcherButton" data-test-subj={'switchQueryLanguageButton'} - aria-label={i18n.translate('unifiedSearch.switchLanguage.buttonText', { - defaultMessage: 'Switch language button.', - })} + aria-label={strings.getSwitchLanguageButtonText()} disabled={isDisabled} /> ); diff --git a/src/plugins/unified_search/public/query_string_input/no_data_popover.tsx b/src/plugins/unified_search/public/query_string_input/no_data_popover.tsx index a5a481d9520f3..7e9760486bb93 100644 --- a/src/plugins/unified_search/public/query_string_input/no_data_popover.tsx +++ b/src/plugins/unified_search/public/query_string_input/no_data_popover.tsx @@ -14,6 +14,26 @@ import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; const NO_DATA_POPOVER_STORAGE_KEY = 'data.noDataPopover'; +export const strings = { + getNoDataPopoverContent: () => + i18n.translate('unifiedSearch.noDataPopover.content', { + defaultMessage: + "This time range doesn't contain any data. Increase or adjust the time range to see more fields and create charts.", + }), + getNoDataPopoverSubtitle: () => + i18n.translate('unifiedSearch.noDataPopover.subtitle', { defaultMessage: 'Tip' }), + + getNoDataPopoverTitle: () => + i18n.translate('unifiedSearch.noDataPopover.title', { + defaultMessage: 'Empty dataset', + }), + + getNoDataPopoverDismissAction: () => + i18n.translate('unifiedSearch.noDataPopover.dismissAction', { + defaultMessage: "Don't show again", + }), +}; + export function NoDataPopover({ showNoDataPopover, storage, @@ -42,12 +62,7 @@ export function NoDataPopover({ }} content={ -

- {i18n.translate('unifiedSearch.noDataPopover.content', { - defaultMessage: - "This time range doesn't contain any data. Increase or adjust the time range to see more fields and create charts.", - })} -

+

{strings.getNoDataPopoverContent()}

} minWidth={300} @@ -56,10 +71,8 @@ export function NoDataPopover({ step={1} stepsTotal={1} isStepOpen={noDataPopoverVisible} - subtitle={i18n.translate('unifiedSearch.noDataPopover.subtitle', { defaultMessage: 'Tip' })} - title={i18n.translate('unifiedSearch.noDataPopover.title', { - defaultMessage: 'Empty dataset', - })} + subtitle={strings.getNoDataPopoverSubtitle()} + title={strings.getNoDataPopoverTitle()} footerAction={ - {i18n.translate('unifiedSearch.noDataPopover.dismissAction', { - defaultMessage: "Don't show again", - })} + {strings.getNoDataPopoverDismissAction()} } > diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx index 2681ee2147e50..da3ecd7771cbf 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx @@ -15,6 +15,7 @@ import { useGeneratedHtmlId, EuiButtonIconProps, EuiToolTip, + useEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { Filter, Query, TimeRange } from '@kbn/es-query'; @@ -22,6 +23,14 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { SavedQueryService, SavedQuery } from '@kbn/data-plugin/public'; import { QueryBarMenuPanels, QueryBarMenuPanelsProps } from './query_bar_menu_panels'; import { FilterEditorWrapper } from './filter_editor_wrapper'; +import { popoverDragAndDropCss } from './add_filter_popover.styles'; + +export const strings = { + getFilterSetButtonLabel: () => + i18n.translate('unifiedSearch.filter.options.filterSetButtonLabel', { + defaultMessage: 'Saved query menu', + }), +}; export interface QueryBarMenuProps { language: string; @@ -79,6 +88,7 @@ export function QueryBarMenu({ isDisabled, }: QueryBarMenuProps) { const [renderedComponent, setRenderedComponent] = useState('menu'); + const euiTheme = useEuiTheme(); useEffect(() => { if (openQueryBarMenu) { @@ -97,12 +107,8 @@ export function QueryBarMenu({ toggleFilterBarMenuPopover(false); }; - const buttonLabel = i18n.translate('unifiedSearch.filter.options.filterSetButtonLabel', { - defaultMessage: 'Saved query menu', - }); - const button = ( - + @@ -185,6 +191,9 @@ export function QueryBarMenu({ anchorPosition="downLeft" repositionOnScroll data-test-subj="queryBarMenuPopover" + panelProps={{ + css: popoverDragAndDropCss(euiTheme), + }} > {renderComponent()} diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx index c36124ca7f448..b074fdd391d7c 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx @@ -7,7 +7,6 @@ */ import React, { useState, useRef, useEffect, useCallback } from 'react'; -import { i18n } from '@kbn/i18n'; import { isEqual } from 'lodash'; import { EuiContextMenuPanelDescriptor, @@ -26,6 +25,7 @@ import { pinFilter, unpinFilter, } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { KIBANA_USER_QUERY_LANGUAGE_KEY, UI_SETTINGS } from '@kbn/data-plugin/common'; @@ -44,6 +44,98 @@ const MAP_ITEMS_TO_FILTER_OPTION: Record = { 'filter-sets-removeAllFilters': 'deleteFilter', }; +export const strings = { + getLuceneLanguageName: () => + i18n.translate('unifiedSearch.query.queryBar.luceneLanguageName', { + defaultMessage: 'Lucene', + }), + getKqlLanguageName: () => + i18n.translate('unifiedSearch.query.queryBar.kqlLanguageName', { + defaultMessage: 'KQL', + }), + getOptionsAddFilterButtonLabel: () => + i18n.translate('unifiedSearch.filter.options.addFilterButtonLabel', { + defaultMessage: 'Add filter', + }), + getOptionsApplyAllFiltersButtonLabel: () => + i18n.translate('unifiedSearch.filter.options.applyAllFiltersButtonLabel', { + defaultMessage: 'Apply to all', + }), + getLoadOtherFilterSetLabel: () => + i18n.translate('unifiedSearch.filter.options.loadOtherFilterSetLabel', { + defaultMessage: 'Load other saved query', + }), + getLoadCurrentFilterSetLabel: () => + i18n.translate('unifiedSearch.filter.options.loadCurrentFilterSetLabel', { + defaultMessage: 'Load saved query', + }), + getSaveAsNewFilterSetLabel: () => + i18n.translate('unifiedSearch.filter.options.saveAsNewFilterSetLabel', { + defaultMessage: 'Save as new', + }), + getSaveFilterSetLabel: () => + i18n.translate('unifiedSearch.filter.options.saveFilterSetLabel', { + defaultMessage: 'Save saved query', + }), + getClearllFiltersButtonLabel: () => + i18n.translate('unifiedSearch.filter.options.clearllFiltersButtonLabel', { + defaultMessage: 'Clear all', + }), + getSavedQueryLabel: () => + i18n.translate('unifiedSearch.search.searchBar.savedQuery', { + defaultMessage: 'Saved query', + }), + getSavedQueryPopoverSaveChangesButtonAriaLabel: (title?: string) => + i18n.translate('unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel', { + defaultMessage: 'Save changes to {title}', + values: { title }, + }), + getSavedQueryPopoverSaveChangesButtonText: () => + i18n.translate('unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText', { + defaultMessage: 'Save changes', + }), + getSavedQueryPopoverSaveAsNewButtonAriaLabel: () => + i18n.translate('unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel', { + defaultMessage: 'Save as new saved query', + }), + getSavedQueryPopoverSaveAsNewButtonText: () => + i18n.translate('unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText', { + defaultMessage: 'Save as new', + }), + getSaveCurrentFilterSetLabel: () => + i18n.translate('unifiedSearch.filter.options.saveCurrentFilterSetLabel', { + defaultMessage: 'Save current saved query', + }), + getApplyAllFiltersButtonLabel: () => + i18n.translate('unifiedSearch.filter.options.applyAllFiltersButtonLabel', { + defaultMessage: 'Apply to all', + }), + getEnableAllFiltersButtonLabel: () => + i18n.translate('unifiedSearch.filter.options.enableAllFiltersButtonLabel', { + defaultMessage: 'Enable all', + }), + getDisableAllFiltersButtonLabel: () => + i18n.translate('unifiedSearch.filter.options.disableAllFiltersButtonLabel', { + defaultMessage: 'Disable all', + }), + getInvertNegatedFiltersButtonLabel: () => + i18n.translate('unifiedSearch.filter.options.invertNegatedFiltersButtonLabel', { + defaultMessage: 'Invert inclusion', + }), + getPinAllFiltersButtonLabel: () => + i18n.translate('unifiedSearch.filter.options.pinAllFiltersButtonLabel', { + defaultMessage: 'Pin all', + }), + getUnpinAllFiltersButtonLabel: () => + i18n.translate('unifiedSearch.filter.options.unpinAllFiltersButtonLabel', { + defaultMessage: 'Unpin all', + }), + getFilterLanguageLabel: () => + i18n.translate('unifiedSearch.filter.options.filterLanguageLabel', { + defaultMessage: 'Filter language', + }), +}; + export interface QueryBarMenuPanelsProps { filters?: Filter[]; savedQuery?: SavedQuery; @@ -226,27 +318,19 @@ export function QueryBarMenuPanels({ }); }; - const luceneLabel = i18n.translate('unifiedSearch.query.queryBar.luceneLanguageName', { - defaultMessage: 'Lucene', - }); - const kqlLabel = i18n.translate('unifiedSearch.query.queryBar.kqlLanguageName', { - defaultMessage: 'KQL', - }); + const luceneLabel = strings.getLuceneLanguageName(); + const kqlLabel = strings.getKqlLanguageName(); const filtersRelatedPanels = [ { - name: i18n.translate('unifiedSearch.filter.options.addFilterButtonLabel', { - defaultMessage: 'Add filter', - }), + name: strings.getOptionsAddFilterButtonLabel(), icon: 'plus', onClick: () => { setRenderedComponent('addFilter'); }, }, { - name: i18n.translate('unifiedSearch.filter.options.applyAllFiltersButtonLabel', { - defaultMessage: 'Apply to all', - }), + name: strings.getOptionsApplyAllFiltersButtonLabel(), icon: 'filter', panel: 2, disabled: !Boolean(filters && filters.length > 0), @@ -257,12 +341,8 @@ export function QueryBarMenuPanels({ const queryAndFiltersRelatedPanels = [ { name: savedQuery - ? i18n.translate('unifiedSearch.filter.options.loadOtherFilterSetLabel', { - defaultMessage: 'Load other saved query', - }) - : i18n.translate('unifiedSearch.filter.options.loadCurrentFilterSetLabel', { - defaultMessage: 'Load saved query', - }), + ? strings.getLoadOtherFilterSetLabel() + : strings.getLoadCurrentFilterSetLabel(), panel: 4, width: 350, icon: 'filter', @@ -270,13 +350,7 @@ export function QueryBarMenuPanels({ disabled: !savedQueries.length, }, { - name: savedQuery - ? i18n.translate('unifiedSearch.filter.options.saveAsNewFilterSetLabel', { - defaultMessage: 'Save as new', - }) - : i18n.translate('unifiedSearch.filter.options.saveFilterSetLabel', { - defaultMessage: 'Save saved query', - }), + name: savedQuery ? strings.getSaveAsNewFilterSetLabel() : strings.getSaveFilterSetLabel(), icon: 'save', disabled: !Boolean(showSaveQuery) || !hasFiltersOrQuery || (savedQuery && !savedQueryHasChanged), @@ -295,9 +369,7 @@ export function QueryBarMenuPanels({ if (showFilterBar || showQueryInput) { items.push( { - name: i18n.translate('unifiedSearch.filter.options.clearllFiltersButtonLabel', { - defaultMessage: 'Clear all', - }), + name: strings.getClearllFiltersButtonLabel(), disabled: !hasFiltersOrQuery && !Boolean(savedQuery), icon: 'crossInACircleFilled', 'data-test-subj': 'filter-sets-removeAllFilters', @@ -341,11 +413,7 @@ export function QueryBarMenuPanels({ data-test-subj="savedQueryTitle" > - {savedQuery - ? savedQuery.attributes.title - : i18n.translate('unifiedSearch.search.searchBar.savedQuery', { - defaultMessage: 'Saved query', - })} + {savedQuery ? savedQuery.attributes.title : strings.getSavedQueryLabel()} @@ -364,41 +432,22 @@ export function QueryBarMenuPanels({ size="s" fill onClick={handleSave} - aria-label={i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel', - { - defaultMessage: 'Save changes to {title}', - values: { title: savedQuery?.attributes.title }, - } + aria-label={strings.getSavedQueryPopoverSaveChangesButtonAriaLabel( + savedQuery?.attributes.title )} data-test-subj="saved-query-management-save-changes-button" > - {i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText', - { - defaultMessage: 'Save changes', - } - )} + {strings.getSavedQueryPopoverSaveChangesButtonText()} - {i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText', - { - defaultMessage: 'Save as new', - } - )} + {strings.getSavedQueryPopoverSaveAsNewButtonText()} @@ -411,23 +460,17 @@ export function QueryBarMenuPanels({ }, { id: 1, - title: i18n.translate('unifiedSearch.filter.options.saveCurrentFilterSetLabel', { - defaultMessage: 'Save current saved query', - }), + title: strings.getSaveCurrentFilterSetLabel(), disabled: !Boolean(showSaveQuery), content:
{saveAsNewQueryFormComponent}
, }, { id: 2, initialFocusedItemIndex: 1, - title: i18n.translate('unifiedSearch.filter.options.applyAllFiltersButtonLabel', { - defaultMessage: 'Apply to all', - }), + title: strings.getApplyAllFiltersButtonLabel(), items: [ { - name: i18n.translate('unifiedSearch.filter.options.enableAllFiltersButtonLabel', { - defaultMessage: 'Enable all', - }), + name: strings.getEnableAllFiltersButtonLabel(), icon: 'eye', 'data-test-subj': 'filter-sets-enableAllFilters', onClick: () => { @@ -436,9 +479,7 @@ export function QueryBarMenuPanels({ }, }, { - name: i18n.translate('unifiedSearch.filter.options.disableAllFiltersButtonLabel', { - defaultMessage: 'Disable all', - }), + name: strings.getDisableAllFiltersButtonLabel(), 'data-test-subj': 'filter-sets-disableAllFilters', icon: 'eyeClosed', onClick: () => { @@ -447,9 +488,7 @@ export function QueryBarMenuPanels({ }, }, { - name: i18n.translate('unifiedSearch.filter.options.invertNegatedFiltersButtonLabel', { - defaultMessage: 'Invert inclusion', - }), + name: strings.getInvertNegatedFiltersButtonLabel(), 'data-test-subj': 'filter-sets-invertAllFilters', icon: 'invert', onClick: () => { @@ -458,9 +497,7 @@ export function QueryBarMenuPanels({ }, }, { - name: i18n.translate('unifiedSearch.filter.options.pinAllFiltersButtonLabel', { - defaultMessage: 'Pin all', - }), + name: strings.getPinAllFiltersButtonLabel(), 'data-test-subj': 'filter-sets-pinAllFilters', icon: 'pin', onClick: () => { @@ -469,9 +506,7 @@ export function QueryBarMenuPanels({ }, }, { - name: i18n.translate('unifiedSearch.filter.options.unpinAllFiltersButtonLabel', { - defaultMessage: 'Unpin all', - }), + name: strings.getUnpinAllFiltersButtonLabel(), 'data-test-subj': 'filter-sets-unpinAllFilters', icon: 'pin', onClick: () => { @@ -483,9 +518,7 @@ export function QueryBarMenuPanels({ }, { id: 3, - title: i18n.translate('unifiedSearch.filter.options.filterLanguageLabel', { - defaultMessage: 'Filter language', - }), + title: strings.getFilterLanguageLabel(), content: ( {manageFilterSetComponent}
, }, diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx index 8426275f10dfd..d13a0b0e8a261 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -27,8 +27,8 @@ import { useIsWithinBreakpoints, EuiSuperUpdateButton, } from '@elastic/eui'; -import { TimeHistoryContract, getQueryLog } from '@kbn/data-plugin/public'; import { i18n } from '@kbn/i18n'; +import { TimeHistoryContract, getQueryLog } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; import type { PersistedLog } from '@kbn/data-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; @@ -48,6 +48,21 @@ import type { SuggestionsListSize } from '../typeahead/suggestions_component'; import { TextBasedLanguagesEditor } from './text_based_languages_editor'; import './query_bar.scss'; +export const strings = { + getNeedsUpdatingLabel: () => + i18n.translate('unifiedSearch.queryBarTopRow.submitButton.update', { + defaultMessage: 'Needs updating', + }), + getRefreshQueryLabel: () => + i18n.translate('unifiedSearch.queryBarTopRow.submitButton.refresh', { + defaultMessage: 'Refresh query', + }), + getRunQueryLabel: () => + i18n.translate('unifiedSearch.queryBarTopRow.submitButton.run', { + defaultMessage: 'Run query', + }), +}; + const SuperDatePicker = React.memo( EuiSuperDatePicker as any ) as unknown as typeof EuiSuperDatePicker; @@ -405,19 +420,9 @@ export const QueryBarTopRow = React.memo( if (!shouldRenderUpdatebutton() && !shouldRenderDatePicker()) { return null; } - const buttonLabelUpdate = i18n.translate('unifiedSearch.queryBarTopRow.submitButton.update', { - defaultMessage: 'Needs updating', - }); - const buttonLabelRefresh = i18n.translate( - 'unifiedSearch.queryBarTopRow.submitButton.refresh', - { - defaultMessage: 'Refresh query', - } - ); - - const buttonLabelRun = i18n.translate('unifiedSearch.queryBarTopRow.submitButton.run', { - defaultMessage: 'Run query', - }); + const buttonLabelUpdate = strings.getNeedsUpdatingLabel(); + const buttonLabelRefresh = strings.getRefreshQueryLabel(); + const buttonLabelRun = strings.getRunQueryLabel(); const iconDirty = Boolean(isQueryLangSelected) ? 'play' : 'kqlFunction'; const tooltipDirty = Boolean(isQueryLangSelected) ? buttonLabelRun : buttonLabelUpdate; diff --git a/src/plugins/unified_search/public/query_string_input/query_string_input.tsx b/src/plugins/unified_search/public/query_string_input/query_string_input.tsx index 4f6737cec44a0..24768e222af8b 100644 --- a/src/plugins/unified_search/public/query_string_input/query_string_input.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_string_input.tsx @@ -7,8 +7,6 @@ */ import React, { PureComponent } from 'react'; -import { i18n } from '@kbn/i18n'; - import classNames from 'classnames'; import { METRIC_TYPE } from '@kbn/analytics'; @@ -26,6 +24,7 @@ import { PopoverAnchorPosition, toSentenceCase, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { compact, debounce, isEmpty, isEqual, isFunction, partition } from 'lodash'; import { CoreStart, DocLinksStart, Toast } from '@kbn/core/public'; @@ -50,6 +49,36 @@ import { AutocompleteService, QuerySuggestion, QuerySuggestionTypes } from '../a import { getTheme } from '../services'; import './query_string_input.scss'; +export const strings = { + getSearchInputPlaceholderForText: () => + i18n.translate('unifiedSearch.query.queryBar.searchInputPlaceholderForText', { + defaultMessage: 'Filter your data', + }), + getSearchInputPlaceholder: (language: string) => + i18n.translate('unifiedSearch.query.queryBar.searchInputPlaceholder', { + defaultMessage: 'Filter your data using {language} syntax', + values: { language }, + }), + getQueryBarComboboxAriaLabel: (pageType: string) => + i18n.translate('unifiedSearch.query.queryBar.comboboxAriaLabel', { + defaultMessage: 'Search and filter the {pageType} page', + values: { pageType }, + }), + getQueryBarSearchInputAriaLabel: (pageType: string) => + i18n.translate('unifiedSearch.query.queryBar.searchInputAriaLabel', { + defaultMessage: 'Start typing to search and filter the {pageType} page', + values: { pageType }, + }), + getQueryBarClearInputLabel: () => + i18n.translate('unifiedSearch.query.queryBar.clearInputLabel', { + defaultMessage: 'Clear input', + }), + getKQLNestedQuerySyntaxInfoTitle: () => + i18n.translate('unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoTitle', { + defaultMessage: 'KQL nested query syntax', + }), +}; + export interface QueryStringInputDependencies { unifiedSearch: { autocomplete: ReturnType; @@ -484,9 +513,7 @@ export default class QueryStringInputUI extends PureComponent

@@ -707,22 +734,13 @@ export default class QueryStringInputUI extends PureComponent { - let placeholder = ''; if (!this.props.query.language || this.props.query.language === 'text') { - placeholder = i18n.translate('unifiedSearch.query.queryBar.searchInputPlaceholderForText', { - defaultMessage: 'Filter your data', - }); - } else { - const language = - this.props.query.language === 'kuery' ? 'KQL' : toSentenceCase(this.props.query.language); - - placeholder = i18n.translate('unifiedSearch.query.queryBar.searchInputPlaceholder', { - defaultMessage: 'Filter your data using {language} syntax', - values: { language }, - }); + return strings.getSearchInputPlaceholderForText(); } + const language = + this.props.query.language === 'kuery' ? 'KQL' : toSentenceCase(this.props.query.language); - return placeholder; + return strings.getSearchInputPlaceholder(language); }; public render() { @@ -766,10 +784,7 @@ export default class QueryStringInputUI extends PureComponent { this.onQueryStringChange(''); if (this.props.autoSubmit) { diff --git a/src/plugins/unified_search/public/utils/combined_filter.ts b/src/plugins/unified_search/public/utils/combined_filter.ts index 05810ed55f7c2..95fc18b39910e 100644 --- a/src/plugins/unified_search/public/utils/combined_filter.ts +++ b/src/plugins/unified_search/public/utils/combined_filter.ts @@ -6,21 +6,14 @@ * Side Public License, v 1. */ -import { isCombinedFilter, FilterItem } from '@kbn/es-query'; - -export enum ConditionTypes { - OR = 'OR', - AND = 'AND', -} +import { type Filter, isCombinedFilter, CombinedFilter } from '@kbn/es-query'; /** - * Defines a conditional operation type (AND/OR) from the filter otherwise returns undefined. - * @param {FilterItem} filter + * Defines a boolean relation type (AND/OR) from the filter otherwise returns undefined. + * @param {Filter} filter */ -export const getConditionalOperationType = (filter: FilterItem) => { - if (Array.isArray(filter)) { - return ConditionTypes.AND; - } else if (isCombinedFilter(filter)) { - return ConditionTypes.OR; +export const getBooleanRelationType = (filter: Filter | CombinedFilter) => { + if (isCombinedFilter(filter)) { + return filter.meta.relation; } }; diff --git a/src/plugins/unified_search/public/utils/index.ts b/src/plugins/unified_search/public/utils/index.ts index 0e3ac5f05c20c..8c9d2d7323e47 100644 --- a/src/plugins/unified_search/public/utils/index.ts +++ b/src/plugins/unified_search/public/utils/index.ts @@ -9,4 +9,4 @@ export { onRaf } from './on_raf'; export { shallowEqual } from './shallow_equal'; -export { ConditionTypes, getConditionalOperationType } from './combined_filter'; +export { getBooleanRelationType } from './combined_filter'; diff --git a/test/accessibility/apps/filter_panel.ts b/test/accessibility/apps/filter_panel.ts index 79748e2249d3e..325999794b557 100644 --- a/test/accessibility/apps/filter_panel.ts +++ b/test/accessibility/apps/filter_panel.ts @@ -24,16 +24,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.openAddFilterPanel(); await a11y.testAppSnapshot(); await PageObjects.discover.closeAddFilterPanel(); - await filterBar.addFilter('OriginCityName', 'is', 'Rome'); - }); - - it('a11y test on filter panel with custom label', async () => { - await filterBar.clickEditFilter('OriginCityName', 'Rome'); - await testSubjects.click('createCustomLabel'); - await a11y.testAppSnapshot(); + await filterBar.addFilter({ field: 'OriginCityName', operation: 'is', value: 'Rome' }); }); it('a11y test on Edit filter as Query DSL panel', async () => { + await filterBar.clickEditFilter('OriginCityName', 'Rome'); await testSubjects.click('editQueryDSL'); await a11y.testAppSnapshot(); await browser.pressKeys(browser.keys.ESCAPE); @@ -41,7 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // the following tests are for the new saved query panel which also has filter panel options it('a11y test on saved query panel- on more than one filters', async () => { - await filterBar.addFilter('DestCountry', 'is', 'AU'); + await filterBar.addFilter({ field: 'DestCountry', operation: 'is', value: 'AU' }); await testSubjects.click('queryBarMenuPopover'); await a11y.testAppSnapshot(); }); diff --git a/test/functional/apps/context/_context_navigation.ts b/test/functional/apps/context/_context_navigation.ts index 5c6304bc36cee..27b71a5a8c2d0 100644 --- a/test/functional/apps/context/_context_navigation.ts +++ b/test/functional/apps/context/_context_navigation.ts @@ -45,7 +45,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('discover'); await PageObjects.header.waitUntilLoadingHasFinished(); for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { - await filterBar.addFilter(columnName, 'is', value); + await filterBar.addFilter({ field: columnName, operation: 'is', value }); } }); diff --git a/test/functional/apps/context/_discover_navigation.ts b/test/functional/apps/context/_discover_navigation.ts index d955b02599856..9f8e86ad7352e 100644 --- a/test/functional/apps/context/_discover_navigation.ts +++ b/test/functional/apps/context/_discover_navigation.ts @@ -47,7 +47,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { - await filterBar.addFilter(columnName, 'is', value); + await filterBar.addFilter({ field: columnName, operation: 'is', value }); } }); diff --git a/test/functional/apps/context/_filters.ts b/test/functional/apps/context/_filters.ts index f9e95080c92e4..f4e97d871ee27 100644 --- a/test/functional/apps/context/_filters.ts +++ b/test/functional/apps/context/_filters.ts @@ -53,7 +53,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('inclusive filter should be toggleable via the filter bar', async function () { - await filterBar.addFilter(TEST_ANCHOR_FILTER_FIELD, 'IS', TEST_ANCHOR_FILTER_VALUE); + await filterBar.addFilter({ + field: TEST_ANCHOR_FILTER_FIELD, + operation: 'is', + value: TEST_ANCHOR_FILTER_VALUE, + }); await PageObjects.context.waitUntilContextLoadingHasFinished(); // disable filter await filterBar.toggleFilterEnabled(TEST_ANCHOR_FILTER_FIELD); @@ -82,7 +86,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); const addPinnedFilter = async () => { - await filterBar.addFilter(TEST_ANCHOR_FILTER_FIELD, 'IS', TEST_ANCHOR_FILTER_VALUE); + await filterBar.addFilter({ + field: TEST_ANCHOR_FILTER_FIELD, + operation: 'is', + value: TEST_ANCHOR_FILTER_VALUE, + }); await filterBar.toggleFilterPinned(TEST_ANCHOR_FILTER_FIELD); }; @@ -117,7 +125,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should preserve filters when the page is refreshed', async function () { await addPinnedFilter(); - await filterBar.addFilter('extension', 'IS', 'png'); + await filterBar.addFilter({ field: 'extension', operation: 'is', value: 'png' }); await PageObjects.context.waitUntilContextLoadingHasFinished(); await expectFiltersToExist(); await browser.refresh(); @@ -126,7 +134,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should update filters when navigating forward and backward in history', async () => { - await filterBar.addFilter('extension', 'IS', 'png'); + await filterBar.addFilter({ field: 'extension', operation: 'is', value: 'png' }); await PageObjects.context.waitUntilContextLoadingHasFinished(); expect(await filterBar.getFilterCount()).to.be(1); expect(await filterBar.hasFilter('extension', 'png')).to.be(true); @@ -141,5 +149,71 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await filterBar.hasFilter('extension', 'png')).to.be(true); expect(await everyFieldMatches((field) => field[1] === 'png')).to.be(true); }); + + it('should add or filter', async () => { + await filterBar.addFilter({ + condition: 'OR', + filters: [ + { field: 'extension', operation: 'is', value: 'png' }, + { field: 'bytes', operation: 'is between', value: { from: '1000', to: '2000' } }, + ], + }); + + await PageObjects.context.waitUntilContextLoadingHasFinished(); + expect(await filterBar.getFilterCount()).to.be(1); + expect(await filterBar.hasFilterWithId('0')).to.be(true); + + await filterBar.clickEditFilterById('0'); + + expect(await filterBar.getFilterEditorPreview()).to.equal( + 'extension: png OR bytes: 1,000B to 2KB' + ); + }); + + it('should add and filter', async () => { + await filterBar.addFilter({ + condition: 'AND', + filters: [ + { field: 'extension', operation: 'is one of', value: ['png', 'jpeg'] }, + { field: 'bytes', operation: 'is between', value: { from: '1000', to: '2000' } }, + ], + }); + + await PageObjects.context.waitUntilContextLoadingHasFinished(); + expect(await filterBar.getFilterCount()).to.be(1); + expect(await filterBar.hasFilterWithId('0')).to.be(true); + + await filterBar.clickEditFilterById('0'); + + expect(await filterBar.getFilterEditorPreview()).to.equal( + 'extension: is one of png, jpeg AND bytes: 1,000B to 2KB' + ); + }); + + it('should add nested filters', async () => { + await filterBar.addFilter({ + condition: 'AND', + filters: [ + { + condition: 'OR', + filters: [ + { field: 'clientip', operation: 'does not exist' }, + { field: 'extension', operation: 'is one of', value: ['png', 'jpeg'] }, + ], + }, + { field: 'bytes', operation: 'is between', value: { from: '1000', to: '2000' } }, + ], + }); + + await PageObjects.context.waitUntilContextLoadingHasFinished(); + expect(await filterBar.getFilterCount()).to.be(1); + expect(await filterBar.hasFilterWithId('0')).to.be(true); + + await filterBar.clickEditFilterById('0'); + + expect(await filterBar.getFilterEditorPreview()).to.equal( + '(NOT clientip: exists OR extension: is one of png, jpeg) AND bytes: 1,000B to 2KB' + ); + }); }); } diff --git a/test/functional/apps/dashboard/group1/dashboard_unsaved_state.ts b/test/functional/apps/dashboard/group1/dashboard_unsaved_state.ts index f3e290e8b8e45..7fbdc66313eba 100644 --- a/test/functional/apps/dashboard/group1/dashboard_unsaved_state.ts +++ b/test/functional/apps/dashboard/group1/dashboard_unsaved_state.ts @@ -47,7 +47,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('view mode state', () => { before(async () => { await queryBar.setQuery(testQuery); - await filterBar.addFilter('bytes', 'exists'); + await filterBar.addFilter({ field: 'bytes', operation: 'exists' }); await queryBar.submitQuery(); }); diff --git a/test/functional/apps/dashboard/group2/dashboard_filtering.ts b/test/functional/apps/dashboard/group2/dashboard_filtering.ts index 9beef9ece92e6..68b80c0168e2d 100644 --- a/test/functional/apps/dashboard/group2/dashboard_filtering.ts +++ b/test/functional/apps/dashboard/group2/dashboard_filtering.ts @@ -41,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const addFilterAndRefresh = async () => { await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.dashboard.waitForRenderComplete(); - await filterBar.addFilter('bytes', 'is', '12345678'); + await filterBar.addFilter({ field: 'bytes', operation: 'is', value: '12345678' }); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.dashboard.waitForRenderComplete(); // first round of requests sometimes times out, refresh all visualizations to fetch again diff --git a/test/functional/apps/dashboard/group2/full_screen_mode.ts b/test/functional/apps/dashboard/group2/full_screen_mode.ts index 35d9ed8a2a15c..53cb707961ea6 100644 --- a/test/functional/apps/dashboard/group2/full_screen_mode.ts +++ b/test/functional/apps/dashboard/group2/full_screen_mode.ts @@ -85,7 +85,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('shows filter bar in fullscreen mode', async () => { - await filterBar.addFilter('bytes', 'is', '12345678'); + await filterBar.addFilter({ field: 'bytes', operation: 'is', value: '12345678' }); await PageObjects.dashboard.waitForRenderComplete(); await PageObjects.dashboard.clickFullScreenMode(); await retry.try(async () => { diff --git a/test/functional/apps/dashboard/group5/dashboard_error_handling.ts b/test/functional/apps/dashboard/group5/dashboard_error_handling.ts index b00aec24809cc..a3265fdcc7f9d 100644 --- a/test/functional/apps/dashboard/group5/dashboard_error_handling.ts +++ b/test/functional/apps/dashboard/group5/dashboard_error_handling.ts @@ -44,7 +44,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.gotoDashboardLandingPage(); await PageObjects.dashboard.loadSavedDashboard('Dashboard with Missing Lens Panel'); await PageObjects.header.waitUntilLoadingHasFinished(); - await filterBar.addFilter('bytes', 'is', '12345678'); + await filterBar.addFilter({ field: 'bytes', operation: 'is', value: '12345678' }); await PageObjects.header.waitUntilLoadingHasFinished(); expect(await filterBar.getFilterCount()).to.be(1); }); diff --git a/test/functional/apps/dashboard/group5/saved_search_embeddable.ts b/test/functional/apps/dashboard/group5/saved_search_embeddable.ts index 1c0b4bdd433bc..c409f3d802276 100644 --- a/test/functional/apps/dashboard/group5/saved_search_embeddable.ts +++ b/test/functional/apps/dashboard/group5/saved_search_embeddable.ts @@ -44,7 +44,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('highlighting on filtering works', async function () { await dashboardAddPanel.addSavedSearch('Rendering-Test:-saved-search'); - await filterBar.addFilter('agent', 'is', 'Mozilla'); + await filterBar.addFilter({ field: 'agent', operation: 'is', value: 'Mozilla' }); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.dashboard.waitForRenderComplete(); const dataTable = await find.byCssSelector(`[data-test-subj="embeddedSavedSearchDocTable"]`); diff --git a/test/functional/apps/dashboard/group5/share.ts b/test/functional/apps/dashboard/group5/share.ts index 40cea873988e1..da66dd538653f 100644 --- a/test/functional/apps/dashboard/group5/share.ts +++ b/test/functional/apps/dashboard/group5/share.ts @@ -62,7 +62,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('unpinned filter should show up only in app state when dashboard is unsaved', async () => { - await filterBar.addFilter('geo.src', 'is', 'AE'); + await filterBar.addFilter({ field: 'geo.src', operation: 'is', value: 'AE' }); await PageObjects.dashboard.waitForRenderComplete(); const sharedUrl = await getSharedUrl(mode); diff --git a/test/functional/apps/dashboard_elements/controls/options_list.ts b/test/functional/apps/dashboard_elements/controls/options_list.ts index 6bc6ec5b482ef..9ea4193ce23df 100644 --- a/test/functional/apps/dashboard_elements/controls/options_list.ts +++ b/test/functional/apps/dashboard_elements/controls/options_list.ts @@ -322,7 +322,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('dashboard filters', async () => { before(async () => { - await filterBar.addFilter('sound.keyword', 'is one of', ['bark', 'bow ow ow', 'ruff']); + await filterBar.addFilter({ + field: 'sound.keyword', + operation: 'is one of', + value: ['bark', 'bow ow ow', 'ruff'], + }); await dashboard.waitForRenderComplete(); await header.waitUntilLoadingHasFinished(); }); @@ -611,7 +615,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('Can mark multiple selections invalid with Filter', async () => { - await filterBar.addFilter('sound.keyword', 'is', ['hiss']); + await filterBar.addFilter({ field: 'sound.keyword', operation: 'is', value: 'hiss' }); await dashboard.waitForRenderComplete(); await header.waitUntilLoadingHasFinished(); await ensureAvailableOptionsEql(['hiss', 'Ignored selections', 'meow', 'bark']); @@ -641,7 +645,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('Does not mark multiple selections invalid with Filter', async () => { - await filterBar.addFilter('sound.keyword', 'is', ['hiss']); + await filterBar.addFilter({ field: 'sound.keyword', operation: 'is', value: 'hiss' }); await dashboard.waitForRenderComplete(); await header.waitUntilLoadingHasFinished(); await ensureAvailableOptionsEql(['hiss']); diff --git a/test/functional/apps/discover/ccs_compatibility/_saved_queries.ts b/test/functional/apps/discover/ccs_compatibility/_saved_queries.ts index 2084d6b258a9c..417d9ad1bbcf2 100644 --- a/test/functional/apps/discover/ccs_compatibility/_saved_queries.ts +++ b/test/functional/apps/discover/ccs_compatibility/_saved_queries.ts @@ -60,7 +60,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(hitCount).to.be('4,731'); }); - await filterBar.addFilter('extension.raw', 'is one of', 'jpg'); + await filterBar.addFilter({ field: 'extension.raw', operation: 'is one of', value: ['jpg'] }); await retry.try(async function tryingForTime() { const hitCount = await PageObjects.discover.getHitCount(); expect(hitCount).to.be('3,029'); diff --git a/test/functional/apps/discover/group1/_filter_editor.ts b/test/functional/apps/discover/group1/_filter_editor.ts index 7b91df45179bb..1f212a5dd8d63 100644 --- a/test/functional/apps/discover/group1/_filter_editor.ts +++ b/test/functional/apps/discover/group1/_filter_editor.ts @@ -39,7 +39,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('filter editor', function () { it('should add a phrases filter', async function () { - await filterBar.addFilter('extension.raw', 'is one of', 'jpg'); + await filterBar.addFilter({ + field: 'extension.raw', + operation: 'is one of', + value: ['jpg'], + }); expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); }); @@ -52,7 +56,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should support filtering on nested fields', async () => { - await filterBar.addFilter('nestedField.child', 'is', 'nestedValue'); + await filterBar.addFilter({ + field: 'nestedField.child', + operation: 'is', + value: 'nestedValue', + }); expect(await filterBar.hasFilter('nestedField.child', 'nestedValue')).to.be(true); await retry.try(async function () { expect(await PageObjects.discover.getHitCount()).to.be('1'); @@ -105,7 +113,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should support range filter on version fields', async () => { - await filterBar.addFilter('version', 'is between', '2.0.0', '3.0.0'); + await filterBar.addFilter({ + field: 'version', + operation: 'is between', + value: { from: '2.0.0', to: '3.0.0' }, + }); expect(await filterBar.hasFilter('version', '2.0.0 to 3.0.0')).to.be(true); await retry.try(async function () { expect(await PageObjects.discover.getHitCount()).to.be('1'); diff --git a/test/functional/apps/discover/group1/_sidebar.ts b/test/functional/apps/discover/group1/_sidebar.ts index db3abf5046582..218e4367ae590 100644 --- a/test/functional/apps/discover/group1/_sidebar.ts +++ b/test/functional/apps/discover/group1/_sidebar.ts @@ -62,7 +62,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.clickFieldListItem('extension'); expect(await testSubjects.getVisibleText('dscFieldStats-topValues')).to.be(allTermsResult); - await filterBar.addFilter('extension', 'is', 'jpg'); + await filterBar.addFilter({ field: 'extension', operation: 'is', value: 'jpg' }); await PageObjects.header.waitUntilLoadingHasFinished(); const onlyJpgResult = 'jpg\n100%'; @@ -409,7 +409,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'jpg\n65.0%\ncss\n15.4%\npng\n9.8%\ngif\n6.6%\nphp\n3.2%' ); - await filterBar.addFilter('extension', 'is', 'jpg'); + await filterBar.addFilter({ field: 'extension', operation: 'is', value: 'jpg' }); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.discover.waitUntilSidebarHasLoaded(); diff --git a/test/functional/apps/discover/group2/_adhoc_data_views.ts b/test/functional/apps/discover/group2/_adhoc_data_views.ts index 50eb3be5f07d1..004eba75d3b6a 100644 --- a/test/functional/apps/discover/group2/_adhoc_data_views.ts +++ b/test/functional/apps/discover/group2/_adhoc_data_views.ts @@ -93,7 +93,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should support query and filtering', async () => { - await filterBar.addFilter('nestedField.child', 'is', 'nestedValue'); + await filterBar.addFilter({ + field: 'nestedField.child', + operation: 'is', + value: 'nestedValue', + }); expect(await filterBar.hasFilter('nestedField.child', 'nestedValue')).to.be(true); await retry.try(async function () { expect(await PageObjects.discover.getHitCount()).to.be('1'); @@ -241,10 +245,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.createAdHocDataView('logstas', true); await PageObjects.header.waitUntilLoadingHasFinished(); - await filterBar.addFilter('nestedField.child', 'is', 'nestedValue'); + await filterBar.addFilter({ + field: 'nestedField.child', + operation: 'is', + value: 'nestedValue', + }); await PageObjects.header.waitUntilLoadingHasFinished(); - await filterBar.addFilter('extension', 'is', 'jpg'); + await filterBar.addFilter({ field: 'extension', operation: 'is', value: 'jpg' }); await PageObjects.header.waitUntilLoadingHasFinished(); const first = await PageObjects.discover.getCurrentDataViewId(); diff --git a/test/functional/apps/visualize/group1/_data_table.ts b/test/functional/apps/visualize/group1/_data_table.ts index 9b95c5b69fd41..19af7438fe202 100644 --- a/test/functional/apps/visualize/group1/_data_table.ts +++ b/test/functional/apps/visualize/group1/_data_table.ts @@ -223,7 +223,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should correctly filter for applied time filter on the main timefield', async () => { - await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21'); + await filterBar.addFilter({ + field: '@timestamp', + operation: 'is between', + value: { from: '2015-09-19', to: '2015-09-21' }, + }); await PageObjects.visChart.waitForVisualizationRenderingStabilized(); const data = await PageObjects.visChart.getTableVisContent(); expect(data).to.be.eql([['2015-09-20', '4,757']]); diff --git a/test/functional/apps/visualize/group1/_data_table_nontimeindex.ts b/test/functional/apps/visualize/group1/_data_table_nontimeindex.ts index 43407d3a899ea..897722d714145 100644 --- a/test/functional/apps/visualize/group1/_data_table_nontimeindex.ts +++ b/test/functional/apps/visualize/group1/_data_table_nontimeindex.ts @@ -127,7 +127,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should correctly filter for applied time filter on the main timefield', async () => { - await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21'); + await filterBar.addFilter({ + field: '@timestamp', + operation: 'is between', + value: { from: '2015-09-19', to: '2015-09-21' }, + }); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); const data = await PageObjects.visChart.getTableVisContent(); diff --git a/test/functional/apps/visualize/group1/_embedding_chart.ts b/test/functional/apps/visualize/group1/_embedding_chart.ts index a07e3a36e2aea..4aea828e48984 100644 --- a/test/functional/apps/visualize/group1/_embedding_chart.ts +++ b/test/functional/apps/visualize/group1/_embedding_chart.ts @@ -62,7 +62,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should allow to filter in embedded mode', async () => { - await filterBar.addFilter('@timestamp', 'is between', '2015-09-21', '2015-09-23'); + await filterBar.addFilter({ + field: '@timestamp', + operation: 'is between', + value: { from: '2015-09-21', to: '2015-09-23' }, + }); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); diff --git a/test/functional/apps/visualize/group3/_linked_saved_searches.ts b/test/functional/apps/visualize/group3/_linked_saved_searches.ts index e64a3f18bde95..c27d5725e50ac 100644 --- a/test/functional/apps/visualize/group3/_linked_saved_searches.ts +++ b/test/functional/apps/visualize/group3/_linked_saved_searches.ts @@ -33,7 +33,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualize.initTests(); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await filterBar.addFilter('extension.raw', 'is', 'jpg'); + await filterBar.addFilter({ field: 'extension.raw', operation: 'is', value: 'jpg' }); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.discover.saveSearch(savedSearchName); discoverSavedSearchUrlPath = (await browser.getCurrentUrl()).split('?')[0]; @@ -80,7 +80,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should allow adding filters while having a linked saved search', async () => { - await filterBar.addFilter('bytes', 'is between', '100', '3000'); + await filterBar.addFilter({ + field: 'bytes', + operation: 'is between', + value: { from: '100', to: '3000' }, + }); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.waitFor('wait for count to equal 707', async () => { const data = await PageObjects.visChart.getTableVisContent(); diff --git a/test/functional/apps/visualize/group6/_vega_chart.ts b/test/functional/apps/visualize/group6/_vega_chart.ts index 1d802065ad137..33e21dbcb46e2 100644 --- a/test/functional/apps/visualize/group6/_vega_chart.ts +++ b/test/functional/apps/visualize/group6/_vega_chart.ts @@ -91,7 +91,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const fullDataLabels = await PageObjects.vegaChart.getYAxisLabels(); expect(fullDataLabels[0]).to.eql('0'); expect(fullDataLabels[fullDataLabels.length - 1]).to.eql('1,600'); - await filterBar.addFilter('@tags.raw', 'is', 'error'); + await filterBar.addFilter({ field: '@tags.raw', operation: 'is', value: 'error' }); await PageObjects.visChart.waitForVisualizationRenderingStabilized(); const filteredDataLabels = await PageObjects.vegaChart.getYAxisLabels(); expect(filteredDataLabels[0]).to.eql('0'); @@ -254,7 +254,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should remove filter by calling "kibanaRemoveFilter" expression', async () => { - await filterBar.addFilter('response', 'is', '200'); + await filterBar.addFilter({ field: 'response', operation: 'is', value: '200' }); expect(await filterBar.getFilterCount()).to.be(1); @@ -266,8 +266,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should remove all filters by calling "kibanaRemoveAllFilters" expression', async () => { - await filterBar.addFilter('response', 'is', '200'); - await filterBar.addFilter('response', 'is', '500'); + await filterBar.addFilter({ field: 'response', operation: 'is', value: '200' }); + await filterBar.addFilter({ field: 'response', operation: 'is', value: '500' }); expect(await filterBar.getFilterCount()).to.be(2); diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts index 42e96fe6b96c2..5751b17a3a126 100644 --- a/test/functional/services/filter_bar.ts +++ b/test/functional/services/filter_bar.ts @@ -6,9 +6,63 @@ * Side Public License, v 1. */ -import classNames from 'classnames'; +import { $Values } from '@kbn/utility-types'; import { FtrService } from '../ftr_provider_context'; +export const Operation = { + IS: 'is', + IS_NOT: 'is not', + IS_ONE_OF: 'is one of', + IS_NOT_ONE_OF: 'is not one of', + IS_BETWEEN: 'is between', + IS_NOT_BETWEEN: 'is not between', + EXISTS: 'exists', + DOES_NOT_EXIST: 'does not exist', +} as const; + +export const BooleanRelation = { + AND: 'AND', + OR: 'OR', +} as const; + +type BooleanRelation = $Values; + +interface BasicFilter { + field: string; +} + +interface FilterWithMultipleValues extends BasicFilter { + operation: typeof Operation.IS_ONE_OF | typeof Operation.IS_NOT_ONE_OF; + value: string[]; +} + +interface FilterWithRange extends BasicFilter { + operation: typeof Operation.IS_BETWEEN | typeof Operation.IS_NOT_BETWEEN; + value: { from: string | undefined; to: string | undefined }; +} + +interface FilterWithSingleValue extends BasicFilter { + operation: typeof Operation.IS | typeof Operation.IS_NOT; + value: string; +} + +interface FilterWithoutValue extends BasicFilter { + operation: typeof Operation.EXISTS | typeof Operation.DOES_NOT_EXIST; +} + +type FilterLeaf = + | FilterWithoutValue + | FilterWithSingleValue + | FilterWithMultipleValues + | FilterWithRange; + +interface FilterNode { + condition: BooleanRelation; + filters: Array; +} + +type Filter = FilterLeaf | FilterNode; + export class FilterBarService extends FtrService { private readonly comboBox = this.ctx.getService('comboBox'); private readonly testSubjects = this.ctx.getService('testSubjects'); @@ -17,7 +71,7 @@ export class FilterBarService extends FtrService { private readonly retry = this.ctx.getService('retry'); private readonly config = this.ctx.getService('config'); private readonly defaultTryTimeout = this.config.get('timeouts.try'); - + private readonly browser = this.ctx.getService('browser'); /** * Checks if specified filter exists * @@ -36,20 +90,40 @@ export class FilterBarService extends FtrService { ): Promise { const filterActivationState = enabled ? 'enabled' : 'disabled'; const filterPinnedState = pinned ? 'pinned' : 'unpinned'; - const filterNegatedState = negated ? 'filter-negated' : ''; - return this.testSubjects.exists( - classNames( - 'filter', - `filter-${filterActivationState}`, - key !== '' && `filter-key-${key}`, - value !== '' && `filter-value-${value}`, - `filter-${filterPinnedState}`, - filterNegatedState - ), - { - allowHidden: true, - } - ); + const filterNegatedState = negated ? '~filter-negated' : ''; + const dataSubj = [ + '~filter', + `~filter-${filterActivationState}`, + key !== '' && `~filter-key-${key}`, + value !== '' && `~filter-value-${value}`, + `~filter-${filterPinnedState}`, + filterNegatedState, + ] + .filter(Boolean) + .join(' & '); + + return this.testSubjects.exists(dataSubj, { allowHidden: true }); + } + + public async hasFilterWithId( + id: string, + enabled: boolean = true, + pinned: boolean = false, + negated: boolean = false + ): Promise { + const filterActivationState = enabled ? 'enabled' : 'disabled'; + const filterPinnedState = pinned ? 'pinned' : 'unpinned'; + const filterNegatedState = negated ? '~filter-negated' : ''; + const dataSubj = [ + '~filter', + `~filter-${filterActivationState}`, + `~filter-${filterPinnedState}`, + filterNegatedState, + `~filter-id-${id}`, + ] + .filter(Boolean) + .join(' & '); + return this.testSubjects.exists(dataSubj, { allowHidden: true }); } /** @@ -113,70 +187,120 @@ export class FilterBarService extends FtrService { return Promise.all(filters.map((filter) => filter.getVisibleText())); } - public async addFilterAndSelectDataView( - dataViewTitle: string | null, - field: string, - operator: string, - ...values: any - ): Promise { - await this.retry.tryForTime(this.defaultTryTimeout * 2, async () => { + public async openFilterBuilder() { + await this.retry.try(async () => { await this.testSubjects.click('addFilter'); await this.testSubjects.existOrFail('addFilterPopover'); + }); + } - if (dataViewTitle) { - await this.comboBox.set('filterIndexPatternsSelect', dataViewTitle); + private async addOrFilter(path: string) { + const filterForm = await this.testSubjects.find(`filter-${path}`); + const addOrBtn = await filterForm.findByTestSubject('add-or-filter'); + await addOrBtn.click(); + } + + private async addAndFilter(path: string) { + const filterForm = await this.testSubjects.find(`filter-${path}`); + const addAndBtn = await filterForm.findByTestSubject('add-and-filter'); + await addAndBtn.click(); + } + + private async addConditionalFilter(filter: FilterNode, path: string) { + if (filter.condition === BooleanRelation.OR) { + return await this.addOrFilter(path); + } + await this.addAndFilter(path); + } + + private isFilterLeafWithoutValue(filter: FilterLeaf): filter is FilterWithoutValue { + return !('value' in filter); + } + + private isFilterWithRange(filter: FilterLeaf): filter is FilterWithRange { + return ( + 'value' in filter && + !Array.isArray(filter.value) && + typeof filter.value === 'object' && + 'from' in filter.value && + 'to' in filter.value + ); + } + + private async pasteFilterData(filter: FilterLeaf, path: string) { + const filterForm = await this.testSubjects.find(`filter-${path}`); + const fieldInput = await filterForm.findByTestSubject('filterFieldSuggestionList'); + await this.comboBox.setElement(fieldInput, filter.field); + + const operatorInput = await filterForm.findByTestSubject('filterOperatorList'); + await this.comboBox.setElement(operatorInput, filter.operation); + if (this.isFilterLeafWithoutValue(filter)) { + return; + } + + if (this.isFilterWithRange(filter)) { + const startInput = await filterForm.findByTestSubject('range-start'); + const endInput = await filterForm.findByTestSubject('range-end'); + + await startInput.type(`${filter.value.from ?? ''}`); + await endInput.type(`${filter.value.to ?? ''}`); + return; + } + + const fieldParams = await filterForm.findByTestSubject('filterParams'); + const filterValueInput = await fieldParams.findByTagName('input'); + + if (Array.isArray(filter.value)) { + for (const value of filter.value) { + await filterValueInput.type(value); + await filterValueInput.type(this.browser.keys.ENTER); } + return; + } - await this.comboBox.set('filterFieldSuggestionList', field); - await this.comboBox.set('filterOperatorList', operator); - const params = await this.testSubjects.find('filterParams'); - const paramsComboBoxes = await params.findAllByCssSelector( - '[data-test-subj~="filterParamsComboBox"]', - 1000 - ); - const paramFields = await params.findAllByTagName('input', 1000); - for (let i = 0; i < values.length; i++) { - let fieldValues = values[i]; - if (!Array.isArray(fieldValues)) { - fieldValues = [fieldValues]; - } + return await filterValueInput.type(filter.value); + } - if (paramsComboBoxes && paramsComboBoxes.length > 0) { - for (let j = 0; j < fieldValues.length; j++) { - await this.comboBox.setElement(paramsComboBoxes[i], fieldValues[j]); - } - } else if (paramFields && paramFields.length > 0) { - for (let j = 0; j < fieldValues.length; j++) { - await paramFields[i].type(fieldValues[j]); - } + private isFilterNode(filter: Filter): filter is FilterNode { + return 'filters' in filter && 'condition' in filter; + } + + private async createFilter(filter: Filter, path: string = '0'): Promise { + if (this.isFilterNode(filter)) { + let startedAdding = false; + for (const [index, f] of filter.filters.entries()) { + if (index < filter.filters.length - 1) { + await this.addConditionalFilter(filter, startedAdding ? `${path}.${index}` : path); } + await this.createFilter(f, `${path}.${index}`); + startedAdding = true; + } + return; + } + + return await this.pasteFilterData(filter, path); + } + + public async addFilterAndSelectDataView( + dataViewTitle: string | null, + filter: Filter + ): Promise { + await this.openFilterBuilder(); + + await this.retry.tryForTime(this.defaultTryTimeout * 2, async () => { + if (dataViewTitle) { + await this.comboBox.set('filterIndexPatternsSelect', dataViewTitle); } + await this.createFilter(filter); + await this.testSubjects.clickWhenNotDisabledWithoutRetry('saveFilter'); }); await this.header.awaitGlobalLoadingIndicatorHidden(); } - /** - * Adds a filter to the filter bar. - * - * @param {string} field The name of the field the filter should be applied for. - * @param {string} operator A valid operator for that fields, e.g. "is one of", "is", "exists", etc. - * @param {string[]|string} values The remaining parameters are the values passed into the individual - * value input fields, i.e. the third parameter into the first input field, the fourth into the second, etc. - * Each value itself can be an array, in case you want to enter multiple values into one field (e.g. for "is one of"): - * @example - * // Add a plain single value - * filterBar.addFilter('country', 'is', 'NL'); - * // Add an exists filter - * filterBar.addFilter('country', 'exists'); - * // Add a range filter for a numeric field - * filterBar.addFilter('bytes', 'is between', '500', '1000'); - * // Add a filter containing multiple values - * filterBar.addFilter('extension', 'is one of', ['jpg', 'png']); - */ - public async addFilter(field: string, operator: string, ...values: any): Promise { - await this.addFilterAndSelectDataView(null, field, operator, ...values); + public async addFilter(filter: Filter): Promise { + await this.addFilterAndSelectDataView(null, filter); } /** @@ -190,6 +314,12 @@ export class FilterBarService extends FtrService { await this.header.awaitGlobalLoadingIndicatorHidden(); } + public async clickEditFilterById(id: string): Promise { + await this.testSubjects.click(`~filter & ~filter-id-${id}`); + await this.testSubjects.click(`editFilter`); + await this.header.awaitGlobalLoadingIndicatorHidden(); + } + /** * Returns available phrases in the filter */ @@ -205,6 +335,11 @@ export class FilterBarService extends FtrService { return optionsString.split('\n'); } + public async getFilterEditorPreview(): Promise { + const filterPreview = await this.testSubjects.find('filter-preview'); + return await filterPreview.getVisibleText(); + } + /** * Closes field editor modal window */ diff --git a/test/plugin_functional/test_suites/data_plugin/session.ts b/test/plugin_functional/test_suites/data_plugin/session.ts index e119d60a90244..6c485a76db32e 100644 --- a/test/plugin_functional/test_suites/data_plugin/session.ts +++ b/test/plugin_functional/test_suites/data_plugin/session.ts @@ -62,7 +62,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide }); it('Starts a new session on filter change', async () => { - await filterBar.addFilter('line_number', 'is', '4.3.108'); + await filterBar.addFilter({ field: 'line_number', operation: 'is', value: '4.3.108' }); await PageObjects.header.waitUntilLoadingHasFinished(); const sessionIds = await getSessionIds(); expect(sessionIds.length).to.be(1); diff --git a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx index 2d9b1aa43660b..3b0221160720c 100644 --- a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx @@ -26,15 +26,19 @@ export function buildFilterLabel({ dataView: DataView; }) { const indexField = dataView.getFieldByName(field)!; + const areMultipleValues = Array.isArray(value) && value.length > 1; + const filter = areMultipleValues + ? buildPhrasesFilter(indexField, value, dataView) + : buildPhraseFilter(indexField, Array.isArray(value) ? value[0] : value, dataView); - const filter = - value instanceof Array && value.length > 1 - ? buildPhrasesFilter(indexField, value, dataView) - : buildPhraseFilter(indexField, value as string, dataView); + filter.meta.type = areMultipleValues ? 'phrases' : 'phrase'; - filter.meta.type = value instanceof Array && value.length > 1 ? 'phrases' : 'phrase'; + filter.meta.value = Array.isArray(value) + ? !areMultipleValues + ? `${value[0]}` + : undefined + : value; - filter.meta.value = value as string; filter.meta.key = label; filter.meta.alias = null; filter.meta.negate = negate; diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts index a21695af5a829..9095b5d83f4ff 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/investigate_in_timeline.cy.ts @@ -54,6 +54,7 @@ describe('Alerts timeline', () => { .first() .invoke('text') .then((severityVal) => { + scrollAlertTableColumnIntoView(ALERT_TABLE_FILE_NAME_HEADER); addAlertPropertyToTimeline(ALERT_TABLE_SEVERITY_VALUES, 0); openActiveTimeline(); cy.get(PROVIDER_BADGE) diff --git a/x-pack/plugins/security_solution/cypress/screens/search_bar.ts b/x-pack/plugins/security_solution/cypress/screens/search_bar.ts index 352a841a1c7be..a9e69625c2da9 100644 --- a/x-pack/plugins/security_solution/cypress/screens/search_bar.ts +++ b/x-pack/plugins/security_solution/cypress/screens/search_bar.ts @@ -15,7 +15,7 @@ export const ADD_FILTER_FORM_FIELD_INPUT = '[data-test-subj="filterFieldSuggestionList"] input[data-test-subj="comboBoxSearchInput"]'; export const ADD_FILTER_FORM_FIELD_OPTION = (value: string) => - `[data-test-subj="comboBoxOptionsList filterFieldSuggestionList-optionsList"] button[title="${value}"] mark`; + `[data-test-subj="comboBoxOptionsList filterFieldSuggestionList-optionsList"] button[title="${value}"]`; export const ADD_FILTER_FORM_OPERATOR_FIELD = '[data-test-subj="filterOperatorList"] input[data-test-subj="comboBoxSearchInput"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 59c8d6a4103f7..ca938164fb2a6 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -185,7 +185,12 @@ export const TIMELINE_FIELDS_BUTTON = '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; export const TIMELINE_FILTER = (filter: TimelineFilter) => - `[data-test-subj="filter filter-enabled filter-key-${filter.field} filter-value-${filter.value} filter-unpinned"]`; + `[data-test-subj~="filter"][data-test-subj~="filter-enabled"][data-test-subj~="filter-key-${ + filter.field + }"][data-test-subj~="filter-value-${(filter.value ?? '').replace( + /\s/g, + '' + )}"][data-test-subj~="filter-unpinned"]`; export const TIMELINE_FILTER_FIELD = '[data-test-subj="filterFieldSuggestionList"]'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index 9819ee09dce1e..8ea4e21509f5d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -11,7 +11,7 @@ import { EuiLoadingSpinner } from '@elastic/eui'; import { coreMock } from '@kbn/core/public/mocks'; import { FilterManager, UI_SETTINGS } from '@kbn/data-plugin/public'; -import { FilterLabel } from '@kbn/unified-search-plugin/public'; +import { FilterBadgeGroup } from '@kbn/unified-search-plugin/public'; import type { DataViewBase } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; import { SeverityBadge } from '../severity_badge'; @@ -131,27 +131,30 @@ describe('helpers', () => { test('returns expected array of ListItems when filters AND indexPatterns exist', () => { const mockQueryBarWithFilters = { ...mockQueryBar, - query: '', + query: '{ bool: { should: [] } }', saved_id: '', }; + + const indexPattern = { + fields: [{ name: 'event.category', type: 'test type' }], + title: 'test title', + getFormatterForField: () => ({ convert: (val: unknown) => val }), + } as unknown as DataViewBase; + const result: ListItems[] = buildQueryBarDescription({ field: 'queryBar', filters: mockQueryBarWithFilters.filters, filterManager: mockFilterManager, query: mockQueryBarWithFilters.query, savedId: mockQueryBarWithFilters.saved_id, - indexPatterns: { - fields: [{ name: 'event.category', type: 'test type' }], - title: 'test title', - getFormatterForField: () => ({ convert: (val: unknown) => val }), - } as unknown as DataViewBase, + indexPatterns: indexPattern, }); const wrapper = shallow(result[0].description as React.ReactElement); - const filterLabelComponent = wrapper.find(FilterLabel).at(0); + const filterBadge = wrapper.find(FilterBadgeGroup).at(0); expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} ); - expect(filterLabelComponent.prop('valueLabel')).toEqual('file'); - expect(filterLabelComponent.prop('filter')).toEqual(mockQueryBar.filters[0]); + expect(filterBadge.prop('filters')).toEqual(mockQueryBarWithFilters.filters); + expect(filterBadge.prop('dataViews')).toEqual([indexPattern]); }); test('returns expected array of ListItems when "query.query" exists', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index a10fa66099ef0..2a0b6591ac55b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -27,8 +27,7 @@ import styled from 'styled-components'; import { FieldIcon } from '@kbn/react-field'; import type { ThreatMapping, Type } from '@kbn/securitysolution-io-ts-alerting-types'; -import { getDisplayValueFromFilter } from '@kbn/data-plugin/public'; -import { FilterLabel } from '@kbn/unified-search-plugin/public'; +import { FilterBadgeGroup } from '@kbn/unified-search-plugin/public'; import { MATCHES, AND, OR } from '../../../../common/components/threat_match/translations'; import type { EqlOptionsSelected } from '../../../../../common/search_strategy'; import { assertUnreachable } from '../../../../../common/utility_types'; @@ -102,11 +101,7 @@ export const buildQueryBarDescription = ({ {indexPatterns != null ? ( - + ) : ( )} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 1b3868de820df..ac79356afa21d 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -5235,7 +5235,7 @@ "unifiedSearch.filter.filterBar.filterItemBadgeIconAriaLabel": "Supprimer {filter}", "unifiedSearch.filter.filterBar.filterString": "Filtre : {innerText}.", "unifiedSearch.filter.filterBar.labelWarningInfo": "Le champ {fieldName} n'existe pas dans la vue en cours.", - "unifiedSearch.filter.filtersBuilder.delimiterLabel": "{conditionType}", + "unifiedSearch.filter.filtersBuilder.delimiterLabel": "{booleanRelation}", "unifiedSearch.kueryAutocomplete.andOperatorDescription": "Nécessite que {bothArguments} soient ''vrai''.", "unifiedSearch.kueryAutocomplete.equalOperatorDescription": "{equals} une certaine valeur", "unifiedSearch.kueryAutocomplete.existOperatorDescription": "{exists} sous un certain format", @@ -5314,8 +5314,6 @@ "unifiedSearch.filter.filterEditor.addButtonLabel": "Ajouter un filtre", "unifiedSearch.filter.filterEditor.addFilterPopupTitle": "Ajouter un filtre", "unifiedSearch.filter.filterEditor.cancelButtonLabel": "Annuler", - "unifiedSearch.filter.filterEditor.createCustomLabelInputLabel": "Étiquette personnalisée", - "unifiedSearch.filter.filterEditor.createCustomLabelSwitchLabel": "Créer une étiquette personnalisée ?", "unifiedSearch.filter.filterEditor.dateViewSelectLabel": "Vue de données", "unifiedSearch.filter.filterEditor.doesNotExistOperatorOptionLabel": "n'existe pas", "unifiedSearch.filter.filterEditor.editFilterPopupTitle": "Modifier le filtre", @@ -5323,35 +5321,25 @@ "unifiedSearch.filter.filterEditor.editQueryDslButtonLabel": "Modifier en tant que Query DSL", "unifiedSearch.filter.filterEditor.existsOperatorOptionLabel": "existe", "unifiedSearch.filter.filterEditor.falseOptionLabel": "faux", - "unifiedSearch.filter.filterEditor.fieldSelectLabel": "Champ", - "unifiedSearch.filter.filterEditor.fieldSelectPlaceholder": "Sélectionner d'abord un champ", "unifiedSearch.filter.filterEditor.isBetweenOperatorOptionLabel": "est entre", "unifiedSearch.filter.filterEditor.isNotBetweenOperatorOptionLabel": "n'est pas entre", "unifiedSearch.filter.filterEditor.isNotOneOfOperatorOptionLabel": "n'est pas l'une des options suivantes", "unifiedSearch.filter.filterEditor.isNotOperatorOptionLabel": "n'est pas", "unifiedSearch.filter.filterEditor.isOneOfOperatorOptionLabel": "est l'une des options suivantes", "unifiedSearch.filter.filterEditor.isOperatorOptionLabel": "est", - "unifiedSearch.filter.filterEditor.operatorSelectLabel": "Opérateur", - "unifiedSearch.filter.filterEditor.operatorSelectPlaceholderSelect": "Sélectionner", - "unifiedSearch.filter.filterEditor.operatorSelectPlaceholderWaiting": "En attente", "unifiedSearch.filter.filterEditor.queryDslAriaLabel": "Éditeur Query DSL d'Elasticsearch", "unifiedSearch.filter.filterEditor.queryDslLabel": "Query DSL d'Elasticsearch", - "unifiedSearch.filter.filterEditor.rangeEndInputPlaceholder": "Fin de la plage", "unifiedSearch.filter.filterEditor.rangeInputLabel": "Plage", - "unifiedSearch.filter.filterEditor.rangeStartInputPlaceholder": "Début de la plage", "unifiedSearch.filter.filterEditor.trueOptionLabel": "vrai", "unifiedSearch.filter.filterEditor.updateButtonLabel": "Mettre à jour le filtre", - "unifiedSearch.filter.filterEditor.valueInputLabel": "Valeur", "unifiedSearch.filter.filterEditor.valueInputPlaceholder": "Saisir une valeur", "unifiedSearch.filter.filterEditor.valueSelectPlaceholder": "Sélectionner une valeur", - "unifiedSearch.filter.filterEditor.valuesSelectLabel": "Valeurs", "unifiedSearch.filter.filterEditor.valuesSelectPlaceholder": "Sélectionner des valeurs", "unifiedSearch.filter.filtersBuilder.addAndFilterGroupButtonIcon": "Ajouter un groupe de filtres avec AND", "unifiedSearch.filter.filtersBuilder.addOrFilterGroupButtonIcon": "Ajouter un groupe de filtres avec OR", "unifiedSearch.filter.filtersBuilder.deleteFilterGroupButtonIcon": "Supprimer le groupe de filtres", "unifiedSearch.filter.filtersBuilder.fieldSelectPlaceholder": "Sélectionner d'abord un champ", "unifiedSearch.filter.filtersBuilder.operatorSelectPlaceholderSelect": "Sélectionner", - "unifiedSearch.filter.filtersBuilder.operatorSelectPlaceholderWaiting": "En attente", "unifiedSearch.filter.options.addFilterButtonLabel": "Ajouter un filtre", "unifiedSearch.filter.options.applyAllFiltersButtonLabel": "Appliquer à tous", "unifiedSearch.filter.options.clearllFiltersButtonLabel": "Tout effacer", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 23ca149fbbe89..165975bf6e1b1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5232,7 +5232,7 @@ "unifiedSearch.filter.filterBar.filterItemBadgeIconAriaLabel": "{filter}を削除", "unifiedSearch.filter.filterBar.filterString": "フィルター:{innerText}。", "unifiedSearch.filter.filterBar.labelWarningInfo": "フィールド{fieldName}は現在のビューに存在しません", - "unifiedSearch.filter.filtersBuilder.delimiterLabel": "{conditionType}", + "unifiedSearch.filter.filtersBuilder.delimiterLabel": "{booleanRelation}", "unifiedSearch.kueryAutocomplete.andOperatorDescription": "{bothArguments} が true であることを条件とする", "unifiedSearch.kueryAutocomplete.equalOperatorDescription": "一部の値に{equals}", "unifiedSearch.kueryAutocomplete.existOperatorDescription": "いずれかの形式中に{exists}", @@ -5311,8 +5311,6 @@ "unifiedSearch.filter.filterEditor.addButtonLabel": "フィルターを追加します", "unifiedSearch.filter.filterEditor.addFilterPopupTitle": "フィルターを追加します", "unifiedSearch.filter.filterEditor.cancelButtonLabel": "キャンセル", - "unifiedSearch.filter.filterEditor.createCustomLabelInputLabel": "カスタムラベル", - "unifiedSearch.filter.filterEditor.createCustomLabelSwitchLabel": "カスタムラベルを作成しますか?", "unifiedSearch.filter.filterEditor.dateViewSelectLabel": "データビュー", "unifiedSearch.filter.filterEditor.doesNotExistOperatorOptionLabel": "存在しない", "unifiedSearch.filter.filterEditor.editFilterPopupTitle": "フィルターを編集", @@ -5320,35 +5318,25 @@ "unifiedSearch.filter.filterEditor.editQueryDslButtonLabel": "クエリ DSL として編集", "unifiedSearch.filter.filterEditor.existsOperatorOptionLabel": "存在する", "unifiedSearch.filter.filterEditor.falseOptionLabel": "false", - "unifiedSearch.filter.filterEditor.fieldSelectLabel": "フィールド", - "unifiedSearch.filter.filterEditor.fieldSelectPlaceholder": "フィールドを選択", "unifiedSearch.filter.filterEditor.isBetweenOperatorOptionLabel": "is between", "unifiedSearch.filter.filterEditor.isNotBetweenOperatorOptionLabel": "is not between", "unifiedSearch.filter.filterEditor.isNotOneOfOperatorOptionLabel": "is not one of", "unifiedSearch.filter.filterEditor.isNotOperatorOptionLabel": "is not", "unifiedSearch.filter.filterEditor.isOneOfOperatorOptionLabel": "is one of", "unifiedSearch.filter.filterEditor.isOperatorOptionLabel": "is", - "unifiedSearch.filter.filterEditor.operatorSelectLabel": "演算子", - "unifiedSearch.filter.filterEditor.operatorSelectPlaceholderSelect": "選択してください", - "unifiedSearch.filter.filterEditor.operatorSelectPlaceholderWaiting": "待機中", "unifiedSearch.filter.filterEditor.queryDslAriaLabel": "ElasticsearchクエリDSLエディター", "unifiedSearch.filter.filterEditor.queryDslLabel": "Elasticsearch クエリ DSL", - "unifiedSearch.filter.filterEditor.rangeEndInputPlaceholder": "範囲の終了値", "unifiedSearch.filter.filterEditor.rangeInputLabel": "範囲", - "unifiedSearch.filter.filterEditor.rangeStartInputPlaceholder": "範囲の開始値", "unifiedSearch.filter.filterEditor.trueOptionLabel": "true", "unifiedSearch.filter.filterEditor.updateButtonLabel": "フィルターを更新", - "unifiedSearch.filter.filterEditor.valueInputLabel": "値", "unifiedSearch.filter.filterEditor.valueInputPlaceholder": "値を入力", "unifiedSearch.filter.filterEditor.valueSelectPlaceholder": "値を選択", - "unifiedSearch.filter.filterEditor.valuesSelectLabel": "値", "unifiedSearch.filter.filterEditor.valuesSelectPlaceholder": "値を選択", "unifiedSearch.filter.filtersBuilder.addAndFilterGroupButtonIcon": "ANDを使用してフィルターグループを追加", "unifiedSearch.filter.filtersBuilder.addOrFilterGroupButtonIcon": "ORを使用してフィルターグループを追加", "unifiedSearch.filter.filtersBuilder.deleteFilterGroupButtonIcon": "フィルターグループの削除", "unifiedSearch.filter.filtersBuilder.fieldSelectPlaceholder": "フィールドを選択", "unifiedSearch.filter.filtersBuilder.operatorSelectPlaceholderSelect": "選択してください", - "unifiedSearch.filter.filtersBuilder.operatorSelectPlaceholderWaiting": "待機中", "unifiedSearch.filter.options.addFilterButtonLabel": "フィルターを追加します", "unifiedSearch.filter.options.applyAllFiltersButtonLabel": "すべてに適用", "unifiedSearch.filter.options.clearllFiltersButtonLabel": "すべて消去", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6cd3f143aaec5..0bab0585babbb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5238,7 +5238,7 @@ "unifiedSearch.filter.filterBar.filterItemBadgeIconAriaLabel": "删除 {filter}", "unifiedSearch.filter.filterBar.filterString": "筛选:{innerText}。", "unifiedSearch.filter.filterBar.labelWarningInfo": "当前视图中不存在字段 {fieldName}", - "unifiedSearch.filter.filtersBuilder.delimiterLabel": "{conditionType}", + "unifiedSearch.filter.filtersBuilder.delimiterLabel": "{booleanRelation}", "unifiedSearch.kueryAutocomplete.andOperatorDescription": "需要{bothArguments}为 true", "unifiedSearch.kueryAutocomplete.equalOperatorDescription": "{equals}某一值", "unifiedSearch.kueryAutocomplete.existOperatorDescription": "以任意形式{exists}", @@ -5317,8 +5317,6 @@ "unifiedSearch.filter.filterEditor.addButtonLabel": "添加筛选", "unifiedSearch.filter.filterEditor.addFilterPopupTitle": "添加筛选", "unifiedSearch.filter.filterEditor.cancelButtonLabel": "取消", - "unifiedSearch.filter.filterEditor.createCustomLabelInputLabel": "定制标签", - "unifiedSearch.filter.filterEditor.createCustomLabelSwitchLabel": "创建定制标签?", "unifiedSearch.filter.filterEditor.dateViewSelectLabel": "数据视图", "unifiedSearch.filter.filterEditor.doesNotExistOperatorOptionLabel": "不存在", "unifiedSearch.filter.filterEditor.editFilterPopupTitle": "编辑筛选", @@ -5326,35 +5324,25 @@ "unifiedSearch.filter.filterEditor.editQueryDslButtonLabel": "编辑为查询 DSL", "unifiedSearch.filter.filterEditor.existsOperatorOptionLabel": "存在", "unifiedSearch.filter.filterEditor.falseOptionLabel": "false", - "unifiedSearch.filter.filterEditor.fieldSelectLabel": "字段", - "unifiedSearch.filter.filterEditor.fieldSelectPlaceholder": "首先选择字段", "unifiedSearch.filter.filterEditor.isBetweenOperatorOptionLabel": "介于", "unifiedSearch.filter.filterEditor.isNotBetweenOperatorOptionLabel": "不介于", "unifiedSearch.filter.filterEditor.isNotOneOfOperatorOptionLabel": "不属于", "unifiedSearch.filter.filterEditor.isNotOperatorOptionLabel": "不是", "unifiedSearch.filter.filterEditor.isOneOfOperatorOptionLabel": "属于", "unifiedSearch.filter.filterEditor.isOperatorOptionLabel": "是", - "unifiedSearch.filter.filterEditor.operatorSelectLabel": "运算符", - "unifiedSearch.filter.filterEditor.operatorSelectPlaceholderSelect": "选择", - "unifiedSearch.filter.filterEditor.operatorSelectPlaceholderWaiting": "正在等候", "unifiedSearch.filter.filterEditor.queryDslAriaLabel": "Elasticsearch 查询 DSL 编辑器", "unifiedSearch.filter.filterEditor.queryDslLabel": "Elasticsearch 查询 DSL", - "unifiedSearch.filter.filterEditor.rangeEndInputPlaceholder": "范围结束", "unifiedSearch.filter.filterEditor.rangeInputLabel": "范围", - "unifiedSearch.filter.filterEditor.rangeStartInputPlaceholder": "范围开始", "unifiedSearch.filter.filterEditor.trueOptionLabel": "true", "unifiedSearch.filter.filterEditor.updateButtonLabel": "更新筛选", - "unifiedSearch.filter.filterEditor.valueInputLabel": "值", "unifiedSearch.filter.filterEditor.valueInputPlaceholder": "输入值", "unifiedSearch.filter.filterEditor.valueSelectPlaceholder": "选择值", - "unifiedSearch.filter.filterEditor.valuesSelectLabel": "值", "unifiedSearch.filter.filterEditor.valuesSelectPlaceholder": "选择值", "unifiedSearch.filter.filtersBuilder.addAndFilterGroupButtonIcon": "通过 AND 添加筛选组", "unifiedSearch.filter.filtersBuilder.addOrFilterGroupButtonIcon": "通过 OR 添加筛选组", "unifiedSearch.filter.filtersBuilder.deleteFilterGroupButtonIcon": "删除筛选组", "unifiedSearch.filter.filtersBuilder.fieldSelectPlaceholder": "首先选择字段", "unifiedSearch.filter.filtersBuilder.operatorSelectPlaceholderSelect": "选择", - "unifiedSearch.filter.filtersBuilder.operatorSelectPlaceholderWaiting": "正在等候", "unifiedSearch.filter.options.addFilterButtonLabel": "添加筛选", "unifiedSearch.filter.options.applyAllFiltersButtonLabel": "应用于所有项", "unifiedSearch.filter.options.clearllFiltersButtonLabel": "全部清除", diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings.ts b/x-pack/test/cloud_security_posture_functional/pages/findings.ts index a2114bac5d5be..ef631e3896cb2 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings.ts @@ -55,7 +55,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('SearchBar', () => { it('add filter', async () => { - await filterBar.addFilter('rule.name', 'is', ruleName1); + await filterBar.addFilter({ field: 'rule.name', operation: 'is', value: ruleName1 }); expect(await filterBar.hasFilter('rule.name', ruleName1)).to.be(true); expect(await table.hasColumnValue('Rule', ruleName1)).to.be(true); diff --git a/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts b/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts index 7e31cbcdc425a..eac81107807b2 100644 --- a/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts +++ b/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts @@ -86,7 +86,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { field: 'geo.src', }); - await filterBar.addFilter('geo.src', 'is not', 'CN'); + await filterBar.addFilter({ field: 'geo.src', operation: 'is not', value: 'CN' }); await PageObjects.lens.save('vis2', false, true); await PageObjects.dashboard.useColorSync(true); diff --git a/x-pack/test/functional/apps/dashboard/group3/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/group3/reporting/download_csv.ts index bdcf95955b451..24472a1e9f5be 100644 --- a/x-pack/test/functional/apps/dashboard/group3/reporting/download_csv.ts +++ b/x-pack/test/functional/apps/dashboard/group3/reporting/download_csv.ts @@ -106,7 +106,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.loadSavedDashboard(dashboardPeriodOf2DaysData); // add a filter - await filterBar.addFilter('category', 'is', `Men's Shoes`); + await filterBar.addFilter({ field: 'category', operation: 'is', value: `Men's Shoes` }); await clickActionsMenu('EcommerceData'); await clickDownloadCsv(); @@ -179,7 +179,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); await PageObjects.common.sleep(1000); - await filterBar.addFilter('name.keyword', 'is', 'Fethany'); + await filterBar.addFilter({ field: 'name.keyword', operation: 'is', value: 'Fethany' }); await PageObjects.common.sleep(1000); }); diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts index d1eb2e7e03c27..ddb5d774765bb 100644 --- a/x-pack/test/functional/apps/discover/reporting.ts +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -291,7 +291,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); // filter - await filterBar.addFilter('category', 'is', `Men's Shoes`); + await filterBar.addFilter({ field: 'category', operation: 'is', value: `Men's Shoes` }); await retry.try(async () => { expect(await PageObjects.discover.getHitCount()).to.equal('154'); }); diff --git a/x-pack/test/functional/apps/discover/saved_searches.ts b/x-pack/test/functional/apps/discover/saved_searches.ts index 414dc8395f184..8469df8f7b246 100644 --- a/x-pack/test/functional/apps/discover/saved_searches.ts +++ b/x-pack/test/functional/apps/discover/saved_searches.ts @@ -64,7 +64,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it(`should unselect saved search when navigating to a 'new'`, async function () { await PageObjects.common.navigateToApp('discover'); await PageObjects.discover.selectIndexPattern('ecommerce'); - await filterBar.addFilter('category', 'is', `Men's Shoes`); + await filterBar.addFilter({ field: 'category', operation: 'is', value: `Men's Shoes` }); await queryBar.setQuery('customer_gender:MALE'); await PageObjects.discover.saveSearch('test-unselect-saved-search'); diff --git a/x-pack/test/functional/apps/discover/value_suggestions.ts b/x-pack/test/functional/apps/discover/value_suggestions.ts index 3151f729aa58c..41d1b071b6b0d 100644 --- a/x-pack/test/functional/apps/discover/value_suggestions.ts +++ b/x-pack/test/functional/apps/discover/value_suggestions.ts @@ -96,7 +96,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.context.waitUntilContextLoadingHasFinished(); // Apply filter in context view - await filterBar.addFilter('geo.dest', 'is', 'US'); + await filterBar.addFilter({ field: 'geo.dest', operation: 'is', value: 'US' }); }); }); }); diff --git a/x-pack/test/functional/apps/discover/visualize_field.ts b/x-pack/test/functional/apps/discover/visualize_field.ts index 8c3ebd7fd06a2..1d5923cb112e1 100644 --- a/x-pack/test/functional/apps/discover/visualize_field.ts +++ b/x-pack/test/functional/apps/discover/visualize_field.ts @@ -74,7 +74,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should preserve app filters in lens', async () => { - await filterBar.addFilter('bytes', 'is between', '3500', '4000'); + await filterBar.addFilter({ + field: 'bytes', + operation: 'is between', + value: { from: '3500', to: '4000' }, + }); await PageObjects.discover.findFieldByName('geo.src'); await PageObjects.discover.clickFieldListItemVisualize('geo.src'); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/functional/apps/lens/group1/fields_list.ts b/x-pack/test/functional/apps/lens/group1/fields_list.ts index 3d571483bf9ac..f5a04788ea831 100644 --- a/x-pack/test/functional/apps/lens/group1/fields_list.ts +++ b/x-pack/test/functional/apps/lens/group1/fields_list.ts @@ -177,7 +177,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 10 ); // define a filter - await filterBar.addFilter('geo.src', 'is', 'CN'); + await filterBar.addFilter({ field: 'geo.src', operation: 'is', value: 'CN' }); await retry.waitFor('Wait for the filter to take effect', async () => { await testSubjects.click(fieldId); // check for top values chart has changed compared to the previous test @@ -194,7 +194,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // One Fields cap's limitation is to not know when an index has no fields based on filters it('should detect fields have no data in popup if filter excludes them', async () => { await filterBar.removeAllFilters(); - await filterBar.addFilter('bytes', 'is', '-1'); + await filterBar.addFilter({ field: 'bytes', operation: 'is', value: '-1' }); // check via popup fields have no data const [fieldId] = await PageObjects.lens.findFieldIdsByType('string'); await log.debug(`Opening field stats for ${fieldId}`); diff --git a/x-pack/test/functional/apps/lens/group1/persistent_context.ts b/x-pack/test/functional/apps/lens/group1/persistent_context.ts index 43d9b28f4e92f..de34f71272427 100644 --- a/x-pack/test/functional/apps/lens/group1/persistent_context.ts +++ b/x-pack/test/functional/apps/lens/group1/persistent_context.ts @@ -160,7 +160,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'Sep 6, 2015 @ 06:31:44.000', 'Sep 18, 2025 @ 06:31:44.000' ); - await filterBar.addFilter('ip', 'is', '97.220.3.248'); + await filterBar.addFilter({ field: 'ip', operation: 'is', value: '97.220.3.248' }); await filterBar.toggleFilterPinned('ip'); await PageObjects.header.clickDiscover(); const timeRange = await PageObjects.timePicker.getTimeConfig(); diff --git a/x-pack/test/functional/apps/lens/group2/dashboard.ts b/x-pack/test/functional/apps/lens/group2/dashboard.ts index a543dc7e50feb..074fed0f22529 100644 --- a/x-pack/test/functional/apps/lens/group2/dashboard.ts +++ b/x-pack/test/functional/apps/lens/group2/dashboard.ts @@ -123,7 +123,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); const hasGeoDestFilter = await filterBar.hasFilter('geo.dest', 'AL'); expect(hasGeoDestFilter).to.be(true); - await filterBar.addFilter('geo.src', 'is', 'US'); + await filterBar.addFilter({ field: 'geo.src', operation: 'is', value: 'US' }); await filterBar.toggleFilterPinned('geo.src'); }); @@ -131,9 +131,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await filterBar.addFilter('geo.src', 'is', 'US'); + await filterBar.addFilter({ field: 'geo.src', operation: 'is', value: 'US' }); await filterBar.toggleFilterPinned('geo.src'); - await filterBar.addFilter('geo.dest', 'is', 'LS'); + await filterBar.addFilter({ field: 'geo.dest', operation: 'is', value: 'LS' }); await dashboardAddPanel.clickCreateNewLink(); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts b/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts index bff59a5b11583..64a319b9467de 100644 --- a/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts +++ b/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts @@ -94,7 +94,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await savedQueryManagementComponent.closeSavedQueryManagementComponent(); await queryBar.setQuery('host.keyword www.elastic.co'); await queryBar.submitQuery(); - await filterBarService.addFilter('geo.src', 'is', 'AF'); + await filterBarService.addFilter({ field: 'geo.src', operation: 'is', value: 'AF' }); // the filter bar seems to need a moment to settle before saving and returning await PageObjects.common.sleep(1000); @@ -104,11 +104,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await savedQueryManagementComponent.closeSavedQueryManagementComponent(); await queryBar.setQuery('request.keyword : "/apm"'); await queryBar.submitQuery(); - await filterBarService.addFilter( - 'host.raw', - 'is', - 'cdn.theacademyofperformingartsandscience.org' - ); + await filterBarService.addFilter({ + field: 'host.raw', + operation: 'is', + value: 'cdn.theacademyofperformingartsandscience.org', + }); await PageObjects.dashboard.clickQuickSave(); diff --git a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/timeseries.ts b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/timeseries.ts index 8d86e8e6843e8..32161cc25225f 100644 --- a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/timeseries.ts +++ b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/timeseries.ts @@ -64,7 +64,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should preserve app filters in lens', async () => { - await filterBar.addFilter('extension', 'is', 'css'); + await filterBar.addFilter({ field: 'extension', operation: 'is', value: 'css' }); await header.waitUntilLoadingHasFinished(); await visualize.navigateToLensFromAnotherVisulization(); await lens.waitForVisualization('xyVisChart'); diff --git a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/top_n.ts b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/top_n.ts index 0716a1ac4a78b..5ceee668b9586 100644 --- a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/top_n.ts +++ b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/top_n.ts @@ -162,7 +162,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should preserve app filters in lens', async () => { - await filterBar.addFilter('extension', 'is', 'css'); + await filterBar.addFilter({ field: 'extension', operation: 'is', value: 'css' }); await header.waitUntilLoadingHasFinished(); await visualize.navigateToLensFromAnotherVisulization(); await lens.waitForVisualization('xyVisChart'); diff --git a/x-pack/test/functional/apps/maps/group2/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/group2/embeddable/dashboard.js index e0d9e01240a4a..39b65c2458447 100644 --- a/x-pack/test/functional/apps/maps/group2/embeddable/dashboard.js +++ b/x-pack/test/functional/apps/maps/group2/embeddable/dashboard.js @@ -99,15 +99,18 @@ export default function ({ getPageObjects, getService }) { }); it('should apply new container state (time, query, filters) to embeddable', async () => { - await filterBar.addFilterAndSelectDataView('logstash-*', 'machine.os', 'is', 'win 8'); + await filterBar.addFilterAndSelectDataView('logstash-*', { + field: 'machine.os', + operation: 'is', + value: 'win 8', + }); await PageObjects.maps.waitForLayersToLoad(); - await filterBar.addFilterAndSelectDataView( - 'meta_for_geo_shapes*', - 'shape_name', - 'is', - 'alpha' - ); + await filterBar.addFilterAndSelectDataView('meta_for_geo_shapes*', { + field: 'shape_name', + operation: 'is', + value: 'alpha', + }); await PageObjects.maps.waitForLayersToLoad(); const { rawResponse: gridResponse } = await PageObjects.maps.getResponseFromDashboardPanel( @@ -131,7 +134,7 @@ export default function ({ getPageObjects, getService }) { await dashboardPanelActions.editPanelByTitle('geo grid vector grid example'); await PageObjects.maps.waitForLayersToLoad(); - await filterBar.addFilter('machine.os', 'is', 'ios'); + await filterBar.addFilter({ field: 'machine.os', operation: 'is', value: 'ios' }); await PageObjects.maps.waitForLayersToLoad(); await testSubjects.click('mapSaveAndReturnButton'); const { rawResponse: gridResponse } = await PageObjects.maps.getResponseFromDashboardPanel( diff --git a/x-pack/test/functional/apps/maps/group4/lens/choropleth_chart.ts b/x-pack/test/functional/apps/maps/group4/lens/choropleth_chart.ts index b026dd7444748..fd00b0a08e8e1 100644 --- a/x-pack/test/functional/apps/maps/group4/lens/choropleth_chart.ts +++ b/x-pack/test/functional/apps/maps/group4/lens/choropleth_chart.ts @@ -50,7 +50,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.dragFieldToWorkspace('geo.dest', 'xyVisChart'); // add filter to force data fetch to set activeData - await filterBar.addFilter('bytes', 'is between', '200', '10000'); + await filterBar.addFilter({ + field: 'bytes', + operation: 'is between', + value: { from: '200', to: '10000' }, + }); await testSubjects.click('lnsSuggestion-worldCountriesByCountOfRecords > lnsSuggestion'); diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_filters.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_filters.ts index 797293e58dc89..be8c06028685f 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_filters.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_filters.ts @@ -39,7 +39,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await ml.dashboardEmbeddables.selectDiscoverIndexPattern('ft_farequote'); await PageObjects.timePicker.setAbsoluteRange(startTime, endTime); - await filterBar.addFilter(PINNED_FILTER.key, 'is', PINNED_FILTER.value); + await filterBar.addFilter({ + field: PINNED_FILTER.key, + operation: 'is', + value: PINNED_FILTER.value, + }); await filterBar.toggleFilterPinned(PINNED_FILTER.key); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -103,7 +107,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await ml.testExecution.logTestStep(`${testData.suiteTitle} adds a pinned filter`); - await filterBar.addFilter(PINNED_FILTER.key, 'is', PINNED_FILTER.value); + await filterBar.addFilter({ + field: PINNED_FILTER.key, + operation: 'is', + value: PINNED_FILTER.value, + }); await filterBar.toggleFilterPinned(PINNED_FILTER.key); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts index 7efc5a0ca1ab8..c059530cc019d 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts @@ -208,7 +208,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_INDEX); const [{ id: alertId }] = await getAlertsByName(ruleName); - await filterBar.addFilter('alert_id', 'is', alertId); + await filterBar.addFilter({ field: 'alert_id', operation: 'is', value: alertId }); await PageObjects.discover.waitUntilSearchingHasFinished(); const link = await getResultsLink(); @@ -348,7 +348,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // change rule configuration await testSubjects.click('openEditRuleFlyoutButton'); await queryBar.setQuery('message:msg-1'); - await filterBar.addFilter('message.keyword', 'is', 'msg-1'); + await filterBar.addFilter({ field: 'message.keyword', operation: 'is', value: 'msg-1' }); await testSubjects.click('thresholdPopover'); await testSubjects.setValue('alertThresholdInput', '1'); diff --git a/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js b/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js index 39b653784e331..a3d226b36e7bc 100644 --- a/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js +++ b/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js @@ -201,7 +201,7 @@ export default ({ getService, getPageObjects }) => { ); const hitCountNumber = await PageObjects.discover.getHitCount(); const originalHitCount = parseInt(hitCountNumber.replace(/\,/g, '')); - await filterBar.addFilter('extension.keyword', 'is', 'jpg'); + await filterBar.addFilter({ field: 'extension.keyword', operation: 'is', value: 'jpg' }); expect(await filterBar.hasFilter('extension.keyword', 'jpg')).to.be(true); await retry.try(async () => { const hitCountNumber = await PageObjects.discover.getHitCount(); From b8c1e34b3bbc0821fc56bb4cb613da372ecb8502 Mon Sep 17 00:00:00 2001 From: Abdul Wahab Zahid Date: Mon, 19 Dec 2022 14:20:09 +0100 Subject: [PATCH 06/55] [Synthetics UI] Monitor management - project location icon and location statuses (#145288) Co-authored-by: Shahzad Co-authored-by: shahzad31 --- .../synthetics/add_monitor.journey.ts | 4 +- .../synthetics/management_list.journey.ts | 6 +- .../common/components/tag_badges.tsx | 89 ++++++++++++++++++ .../hooks/use_overview_status.ts | 39 ++++++++ .../management/monitor_list_container.tsx | 12 ++- .../management/monitor_list_table/columns.tsx | 92 ++++++++----------- .../management/monitor_list_table/labels.tsx | 8 +- .../monitor_details_link.tsx | 18 +++- .../monitor_list_table/monitor_list.tsx | 10 +- .../monitor_list_table/monitor_locations.tsx | 58 ++++++++++-- .../overview/overview/overview_status.tsx | 27 +----- .../synthetics/state/monitor_list/index.ts | 5 +- .../translations/translations/fr-FR.json | 3 - .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 15 files changed, 272 insertions(+), 105 deletions(-) create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/tag_badges.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_overview_status.ts diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/add_monitor.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/add_monitor.journey.ts index 79ab4fb3378fc..d37d682295e4c 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/add_monitor.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/add_monitor.journey.ts @@ -6,7 +6,7 @@ */ import uuid from 'uuid'; import { journey, step, expect, Page } from '@elastic/synthetics'; -import { FormMonitorType } from '../../../common/runtime_types/monitor_management'; +import { FormMonitorType } from '../../../common/runtime_types'; import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app'; const customLocation = process.env.SYNTHETICS_TEST_LOCATION; @@ -201,7 +201,7 @@ const createMonitorJourney = ({ step('delete monitor', async () => { await syntheticsApp.navigateToMonitorManagement(); - await syntheticsApp.findByText('Monitor name'); + await syntheticsApp.findByText('Monitor'); const isSuccessful = await syntheticsApp.deleteMonitors(); expect(isSuccessful).toBeTruthy(); }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/management_list.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/management_list.journey.ts index d920798d89238..972fce27c56b8 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/management_list.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/management_list.journey.ts @@ -50,10 +50,10 @@ journey(`MonitorManagementList`, async ({ page, params }) => { }); step( - 'Click text=Showing 1-3 of 3 MonitorsSortingThis table contains 3 rows out of 3 rows; Page 1', + 'Click text=Showing 1-3 of 3 ConfigurationsSortingThis table contains 3 rows out of 3 rows; Page 1', async () => { await page.click( - 'text=Showing 1-3 of 3 MonitorsSortingThis table contains 3 rows out of 3 rows; Page 1' + 'text=Showing 1-3 of 3 ConfigurationsSortingThis table contains 3 rows out of 3 rows; Page 1' ); await page.click('[aria-label="expands filter group for Type filter"]'); } @@ -78,7 +78,7 @@ journey(`MonitorManagementList`, async ({ page, params }) => { ]); await page.click('text=1-1'); await page.click( - 'text=Showing 1-1 of 1 MonitorSortingThis table contains 1 rows out of 1 rows; Page 1 ' + 'text=Showing 1-1 of 1 ConfigurationSortingThis table contains 1 rows out of 1 rows; Page 1 ' ); } ); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/tag_badges.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/tag_badges.tsx new file mode 100644 index 0000000000000..3dc6bbd1836bf --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/tag_badges.tsx @@ -0,0 +1,89 @@ +/* + * 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, { useState } from 'react'; +import { EuiBadge, EuiBadgeGroup, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface Props { + onClick?: (tag: string) => void; + tags?: string[]; +} + +const getFilterLabel = (tag: string) => { + return i18n.translate('xpack.synthetics.tagsList.filter', { + defaultMessage: 'Click to filter list with tag {tag}', + values: { + tag, + }, + }); +}; + +export const TagsBadges = ({ tags, onClick }: Props) => { + const [toDisplay, setToDisplay] = useState(3); + + if (!tags || tags.length === 0) { + return --; + } + + const tagsToDisplay = tags.slice(0, toDisplay); + + return ( + + {tagsToDisplay.map((tag) => ( + // filtering only makes sense in monitor list, where we have summary + + { + onClick?.(tag); + }} + onClickAriaLabel={getFilterLabel(tag)} + color="hollow" + className="eui-textTruncate" + style={{ maxWidth: 120 }} + > + {tag} + + ))} + {tags.length > toDisplay && ( + { + setToDisplay(tags.length); + }} + onClickAriaLabel={EXPAND_TAGS_LABEL} + > + +{tags.length - toDisplay} + + )} + {tags.length === toDisplay && ( + { + setToDisplay(3); + }} + onClickAriaLabel={COLLAPSE_TAGS_LABEL} + > + -{tags.length - 3} + + )} + + ); +}; + +const EXPAND_TAGS_LABEL = i18n.translate('xpack.synthetics.management.monitorList.tags.expand', { + defaultMessage: 'Click to view remaining tags', +}); + +const COLLAPSE_TAGS_LABEL = i18n.translate( + 'xpack.synthetics.management.monitorList.tags.collpase', + { + defaultMessage: 'Click to collapse tags', + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_overview_status.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_overview_status.ts new file mode 100644 index 0000000000000..d6390a080b19b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_overview_status.ts @@ -0,0 +1,39 @@ +/* + * 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 { useEffect, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useSyntheticsRefreshContext } from '../../../contexts/synthetics_refresh_context'; +import { + fetchOverviewStatusAction, + quietFetchOverviewStatusAction, + MonitorOverviewPageState, + selectOverviewStatus, +} from '../../../state'; + +export function useOverviewStatus({ pageState }: { pageState: MonitorOverviewPageState }) { + const { status, statusError } = useSelector(selectOverviewStatus); + + const { lastRefresh } = useSyntheticsRefreshContext(); + const lastRefreshRef = useRef(lastRefresh); + + const dispatch = useDispatch(); + + useEffect(() => { + if (lastRefresh !== lastRefreshRef.current) { + dispatch(quietFetchOverviewStatusAction.get(pageState)); + lastRefreshRef.current = lastRefresh; + } else { + dispatch(fetchOverviewStatusAction.get(pageState)); + } + }, [dispatch, lastRefresh, pageState]); + + return { + status, + statusError, + }; +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_container.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_container.tsx index cd299b4f66708..98a87390f0219 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_container.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_container.tsx @@ -5,12 +5,13 @@ * 2.0. */ +import React, { useMemo } from 'react'; import { EuiSpacer } from '@elastic/eui'; -import React from 'react'; import type { useMonitorList } from '../hooks/use_monitor_list'; import { MonitorAsyncError } from './monitor_errors/monitor_async_error'; import { useInlineErrors } from '../hooks/use_inline_errors'; +import { useOverviewStatus } from '../hooks/use_overview_status'; import { ListFilters } from './list_filters/list_filters'; import { MonitorList } from './monitor_list_table/monitor_list'; @@ -38,6 +39,14 @@ export const MonitorListContainer = ({ sortOrder: pageState.sortOrder, }); + const overviewStatusArgs = useMemo(() => { + return { + pageState: { ...pageState, perPage: pageState.pageSize }, + }; + }, [pageState]); + + const { status } = useOverviewStatus(overviewStatusArgs); + if (!isEnabled && absoluteTotal === 0) { return null; } @@ -53,6 +62,7 @@ export const MonitorListContainer = ({ pageState={pageState} error={error} loading={monitorsLoading || errorsLoading} + status={status} errorSummaries={errorSummaries} loadPage={loadPage} reloadPage={reloadPage} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx index ad62c16c1ff69..dba41e5cbfcc6 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx @@ -5,17 +5,18 @@ * 2.0. */ -import { EuiBadge, EuiBasicTableColumn, EuiIcon, EuiThemeComputed } from '@elastic/eui'; +import { EuiBadge, EuiBasicTableColumn, EuiThemeComputed } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import moment from 'moment'; import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { TagsBadges } from '../../../common/components/tag_badges'; import { MonitorDetailsLink } from './monitor_details_link'; import { ConfigKey, DataStream, EncryptedSyntheticsSavedMonitor, + OverviewStatusState, Ping, ServiceLocations, SourceType, @@ -27,15 +28,13 @@ import { Actions } from './actions'; import { MonitorEnabled } from './monitor_enabled'; import { MonitorLocations } from './monitor_locations'; -export function getMonitorListColumns({ +export function useMonitorListColumns({ basePath, euiTheme, - errorSummaries, - errorSummariesById, canEditSynthetics, reloadPage, loading, - syntheticsMonitors, + status, }: { basePath: string; euiTheme: EuiThemeComputed; @@ -44,61 +43,23 @@ export function getMonitorListColumns({ canEditSynthetics: boolean; syntheticsMonitors: EncryptedSyntheticsSavedMonitor[]; loading: boolean; + status: OverviewStatusState | null; reloadPage: () => void; }) { - const getIsMonitorUnHealthy = (monitor: EncryptedSyntheticsSavedMonitor) => { - const errorSummary = errorSummariesById.get(monitor.id); - - if (errorSummary) { - return moment(monitor.updated_at).isBefore(moment(errorSummary.timestamp)); - } - - return false; - }; + const history = useHistory(); return [ { align: 'left' as const, field: ConfigKey.NAME as string, name: i18n.translate('xpack.synthetics.management.monitorList.monitorName', { - defaultMessage: 'Monitor name', + defaultMessage: 'Monitor', }), sortable: true, render: (_: string, monitor: EncryptedSyntheticsSavedMonitor) => ( ), }, - { - align: 'left' as const, - field: 'id', - name: i18n.translate('xpack.synthetics.management.monitorList.monitorStatus', { - defaultMessage: 'Status', - }), - sortable: false, - render: (_: string, monitor: EncryptedSyntheticsSavedMonitor) => { - const isMonitorHealthy = !getIsMonitorUnHealthy(monitor); - - return ( - <> - - {isMonitorHealthy ? ( - - ) : ( - - )} - - ); - }, - }, { align: 'left' as const, field: ConfigKey.MONITOR_TYPE, @@ -110,23 +71,44 @@ export function getMonitorListColumns({ {monitorType === DataStream.BROWSER ? 'Browser' : 'Ping'} ), }, + { + align: 'left' as const, + field: ConfigKey.SCHEDULE, + sortable: true, + name: i18n.translate('xpack.synthetics.management.monitorList.frequency', { + defaultMessage: 'Frequency', + }), + render: (schedule: SyntheticsMonitorSchedule) => getFrequencyLabel(schedule), + }, { align: 'left' as const, field: ConfigKey.LOCATIONS, name: i18n.translate('xpack.synthetics.management.monitorList.locations', { defaultMessage: 'Locations', }), - render: (locations: ServiceLocations) => - locations ? : null, + render: (locations: ServiceLocations, monitor: EncryptedSyntheticsSavedMonitor) => + locations ? ( + + ) : null, }, { align: 'left' as const, - field: ConfigKey.SCHEDULE, - sortable: true, - name: i18n.translate('xpack.synthetics.management.monitorList.frequency', { - defaultMessage: 'Frequency', + field: ConfigKey.TAGS, + name: i18n.translate('xpack.synthetics.management.monitorList.tags', { + defaultMessage: 'Tags', }), - render: (schedule: SyntheticsMonitorSchedule) => getFrequencyLabel(schedule), + render: (tags: string[]) => ( + { + history.push({ search: `tags=${JSON.stringify([tag])}` }); + }} + /> + ), }, { align: 'left' as const, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/labels.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/labels.tsx index fd910f512caa4..db8ae09f0e8b9 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/labels.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/labels.tsx @@ -137,8 +137,8 @@ export const getRecordRangeLabel = ({ total: , monitorsLabel: ( - {i18n.translate('xpack.synthetics.management.monitorList.recordRangeLabel', { - defaultMessage: '{monitorCount, plural, one {Monitor} other {Monitors}}', + {i18n.translate('xpack.synthetics.management.monitorList.configurationRangeLabel', { + defaultMessage: '{monitorCount, plural, one {Configuration} other {Configurations}}', values: { monitorCount: total, }, @@ -190,6 +190,10 @@ export const DISABLE_MONITOR_LABEL = i18n.translate( } ); +export const PROJECT = i18n.translate('xpack.synthetics.management.project', { + defaultMessage: 'Project', +}); + export const getMonitorEnabledSuccessLabel = (name: string) => i18n.translate('xpack.synthetics.management.monitorEnabledSuccessMessage', { defaultMessage: 'Monitor {name} enabled successfully.', diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_details_link.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_details_link.tsx index 4b0b1b2d53a3a..84dd30499bae6 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_details_link.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_details_link.tsx @@ -6,14 +6,16 @@ */ import React from 'react'; -import { EuiLink } from '@elastic/eui'; +import { EuiLink, EuiIcon } from '@elastic/eui'; import { useSelector } from 'react-redux'; import { selectSelectedLocationId } from '../../../../state'; import { ConfigKey, EncryptedSyntheticsSavedMonitor, + SourceType, } from '../../../../../../../common/runtime_types'; import { useMonitorDetailLocator } from '../../hooks/use_monitor_detail_locator'; +import * as labels from './labels'; export const MonitorDetailsLink = ({ basePath, @@ -36,9 +38,23 @@ export const MonitorDetailsLink = ({ locationId, }); + const isProjectMonitor = monitor[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT; + const projectLabel = isProjectMonitor + ? `${labels.PROJECT}: ${monitor[ConfigKey.PROJECT_ID]}` + : ''; + return ( <> {monitor.name} + {isProjectMonitor ? ( + + ) : null} ); }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx index a85c7394705e8..ea1ca7cdc3908 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx @@ -23,9 +23,10 @@ import { ConfigKey, Ping, EncryptedSyntheticsSavedMonitor, + OverviewStatusState, } from '../../../../../../../common/runtime_types'; import { SyntheticsSettingsContext } from '../../../../contexts/synthetics_settings_context'; -import { getMonitorListColumns } from './columns'; +import { useMonitorListColumns } from './columns'; import * as labels from './labels'; interface Props { @@ -37,6 +38,7 @@ interface Props { loadPage: (state: MonitorListPageState) => void; reloadPage: () => void; errorSummaries?: Ping[]; + status: OverviewStatusState | null; } export const MonitorList = ({ @@ -45,14 +47,15 @@ export const MonitorList = ({ total, error, loading, + status, loadPage, reloadPage, errorSummaries, }: Props) => { + const { euiTheme } = useEuiTheme(); const { basePath } = useContext(SyntheticsSettingsContext); const isXl = useIsWithinMinBreakpoint('xxl'); const canEditSynthetics = useCanEditSynthetics(); - const { euiTheme } = useEuiTheme(); const errorSummariesById = useMemo( () => @@ -103,7 +106,7 @@ export const MonitorList = ({ total, }); - const columns = getMonitorListColumns({ + const columns = useMonitorListColumns({ basePath, euiTheme, errorSummaries, @@ -112,6 +115,7 @@ export const MonitorList = ({ syntheticsMonitors, loading, reloadPage, + status, }); return ( diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_locations.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_locations.tsx index f35cd357ef2dd..c1ea2e8c537af 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_locations.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_locations.tsx @@ -5,20 +5,34 @@ * 2.0. */ -import React, { useState } from 'react'; -import { EuiBadge, EuiBadgeGroup } from '@elastic/eui'; -import { ServiceLocations, ServiceLocation } from '../../../../../../../common/runtime_types'; +import React, { useState, useMemo } from 'react'; +import { EuiBadge, EuiBadgeGroup, EuiIcon, useEuiTheme } from '@elastic/eui'; +import { useTheme } from '@kbn/observability-plugin/public'; +import { + ServiceLocations, + ServiceLocation, + OverviewStatusState, +} from '../../../../../../../common/runtime_types'; import { useLocations } from '../../../../hooks'; import { EXPAND_LOCATIONS_LABEL } from './labels'; interface Props { locations: ServiceLocations; + monitorId: string; + status: OverviewStatusState | null; } const INITIAL_LIMIT = 3; -export const MonitorLocations = ({ locations }: Props) => { +export const MonitorLocations = ({ locations, monitorId, status }: Props) => { + const { euiTheme } = useEuiTheme(); + const theme = useTheme(); const { locations: allLocations } = useLocations(); + const locationLabelsById = useMemo(() => { + return allLocations.reduce((acc, cur) => { + return { ...acc, [cur.id]: cur.label }; + }, {} as Record); + }, [allLocations]); const [toDisplay, setToDisplay] = useState(INITIAL_LIMIT); const locationsToDisplay = locations.slice(0, toDisplay); @@ -30,9 +44,20 @@ export const MonitorLocations = ({ locations }: Props) => { key={location.id} color="hollow" className="eui-textTruncate" - css={{ display: 'flex', maxWidth: 120 }} + css={{ display: 'flex', maxWidth: 120, paddingLeft: euiTheme.size.xs, borderRadius: 3 }} > - {`${allLocations.find((loc) => loc.id === location.id)?.label}`} + + {locationLabelsById[location.id]} ))} {locations.length > toDisplay && ( @@ -49,3 +74,24 @@ export const MonitorLocations = ({ locations }: Props) => { ); }; + +function getLocationStatusColor( + euiTheme: ReturnType, + locationLabel: string | undefined, + monitorId: string, + overviewStatus: OverviewStatusState | null +) { + const { + eui: { euiColorVis9, euiColorVis0, euiSideNavDisabledTextcolor }, + } = euiTheme; + + const locById = `${monitorId}-${locationLabel}`; + + if (overviewStatus?.downConfigs[locById]) { + return euiColorVis9; + } else if (overviewStatus?.upConfigs[locById]) { + return euiColorVis0; + } + + return euiSideNavDisabledTextcolor; +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx index 0b7e2e0716440..e82ba5c48d2a8 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx @@ -7,18 +7,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiStat, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { - clearOverviewStatusErrorAction, - fetchOverviewStatusAction, - quietFetchOverviewStatusAction, - selectOverviewPageState, - selectOverviewStatus, -} from '../../../../state'; +import { clearOverviewStatusErrorAction, selectOverviewPageState } from '../../../../state'; import { kibanaService } from '../../../../../../utils/kibana_service'; -import { useSyntheticsRefreshContext } from '../../../../contexts'; import { useGetUrlParams } from '../../../../hooks/use_url_params'; +import { useOverviewStatus } from '../../hooks/use_overview_status'; function title(t?: number) { return t ?? '-'; @@ -26,8 +20,9 @@ function title(t?: number) { export function OverviewStatus() { const { statusFilter } = useGetUrlParams(); - const { status, statusError } = useSelector(selectOverviewStatus); + const pageState = useSelector(selectOverviewPageState); + const { status, statusError } = useOverviewStatus({ pageState }); const dispatch = useDispatch(); const [statusConfig, setStatusConfig] = useState({ up: status?.up, @@ -35,18 +30,6 @@ export function OverviewStatus() { disabledCount: status?.disabledCount, }); - const { lastRefresh } = useSyntheticsRefreshContext(); - const lastRefreshRef = useRef(lastRefresh); - - useEffect(() => { - if (lastRefresh !== lastRefreshRef.current) { - dispatch(quietFetchOverviewStatusAction.get(pageState)); - lastRefreshRef.current = lastRefresh; - } else { - dispatch(fetchOverviewStatusAction.get(pageState)); - } - }, [dispatch, lastRefresh, pageState]); - useEffect(() => { if (statusError) { dispatch(clearOverviewStatusErrorAction()); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts index 997f853c9bfc5..6010737b39ba3 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isEqual } from 'lodash'; import { createReducer } from '@reduxjs/toolkit'; import { FETCH_STATUS } from '@kbn/observability-plugin/public'; @@ -47,7 +48,9 @@ const initialState: MonitorListState = { export const monitorListReducer = createReducer(initialState, (builder) => { builder .addCase(fetchMonitorListAction.get, (state, action) => { - state.pageState = action.payload; + if (!isEqual(state.pageState, action.payload)) { + state.pageState = action.payload; + } state.loading = true; state.loaded = false; }) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index ac79356afa21d..96e5c2ebfe0a8 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -33599,11 +33599,8 @@ "xpack.synthetics.management.monitorList.loading": "Chargement...", "xpack.synthetics.management.monitorList.locations": "Emplacements", "xpack.synthetics.management.monitorList.locations.expand": "Cliquer pour afficher les emplacements restants", - "xpack.synthetics.management.monitorList.monitorHealthy": "Intègre", "xpack.synthetics.management.monitorList.monitorName": "Nom de moniteur", - "xpack.synthetics.management.monitorList.monitorStatus": "Statut", "xpack.synthetics.management.monitorList.monitorType": "Type", - "xpack.synthetics.management.monitorList.monitorUnhealthy": "Défectueux", "xpack.synthetics.management.monitorList.noItemForSelectedFiltersMessage": "Aucun moniteur trouvé pour les critères de filtre sélectionnés", "xpack.synthetics.management.monitorList.noItemMessage": "Aucun moniteur trouvé", "xpack.synthetics.management.monitorList.tags": "Balises", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 165975bf6e1b1..4280e72f3c6e0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -33571,11 +33571,8 @@ "xpack.synthetics.management.monitorList.loading": "読み込み中...", "xpack.synthetics.management.monitorList.locations": "場所", "xpack.synthetics.management.monitorList.locations.expand": "クリックすると、残りの場所が表示されます", - "xpack.synthetics.management.monitorList.monitorHealthy": "正常", "xpack.synthetics.management.monitorList.monitorName": "モニター名", - "xpack.synthetics.management.monitorList.monitorStatus": "ステータス", "xpack.synthetics.management.monitorList.monitorType": "型", - "xpack.synthetics.management.monitorList.monitorUnhealthy": "異常", "xpack.synthetics.management.monitorList.noItemForSelectedFiltersMessage": "選択されたフィルター条件でモニターが見つかりませんでした", "xpack.synthetics.management.monitorList.noItemMessage": "モニターが見つかりません", "xpack.synthetics.management.monitorList.tags": "タグ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0bab0585babbb..e7cf8542e470a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -33605,11 +33605,8 @@ "xpack.synthetics.management.monitorList.loading": "正在加载……", "xpack.synthetics.management.monitorList.locations": "位置", "xpack.synthetics.management.monitorList.locations.expand": "单击以查看剩余位置", - "xpack.synthetics.management.monitorList.monitorHealthy": "运行正常", "xpack.synthetics.management.monitorList.monitorName": "监测名称", - "xpack.synthetics.management.monitorList.monitorStatus": "状态", "xpack.synthetics.management.monitorList.monitorType": "类型", - "xpack.synthetics.management.monitorList.monitorUnhealthy": "运行不正常", "xpack.synthetics.management.monitorList.noItemForSelectedFiltersMessage": "未找到匹配选定筛选条件的监测", "xpack.synthetics.management.monitorList.noItemMessage": "未找到任何监测", "xpack.synthetics.management.monitorList.tags": "标签", From 74ab0759f16cb4331f1b93fb82ea68e8304ac996 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 19 Dec 2022 14:50:29 +0100 Subject: [PATCH 07/55] Image Embeddable (#146421) close https://github.com/elastic/kibana/issues/81345 Adds an image embeddable - a new embeddable type that allows to insert images into dashboard using the new file service --- .github/CODEOWNERS | 1 + .i18nrc.json | 1 + docs/developer/plugin-list.asciidoc | 5 + packages/kbn-optimizer/limits.yml | 1 + .../shared-ux/file/image/impl/src/image.tsx | 2 +- .../_dashboard_actions_strings.ts | 18 +- .../top_nav/dashboard_editing_toolbar.tsx | 107 ++++- .../dashboard_app/top_nav/editor_menu.tsx | 53 +-- .../public/lib/actions/edit_panel_action.ts | 18 +- .../lib/embeddables/embeddable_factory.ts | 4 +- .../public/lib/embeddables/i_embeddable.ts | 2 + src/plugins/files/kibana.json | 1 + src/plugins/image_embeddable/README.md | 4 + src/plugins/image_embeddable/jest.config.js | 18 + src/plugins/image_embeddable/kibana.json | 13 + .../public/image_editor/configure_image.tsx | 87 ++++ .../image_editor/image_editor_flyout.test.tsx | 136 ++++++ .../image_editor/image_editor_flyout.tsx | 445 ++++++++++++++++++ .../public/image_editor/index.ts | 9 + .../image_embeddable/image_embeddable.tsx | 70 +++ .../image_embeddable_factory.tsx | 92 ++++ .../public/image_embeddable/index.ts | 10 + .../public/image_viewer/image_viewer.test.tsx | 77 +++ .../public/image_viewer/image_viewer.tsx | 212 +++++++++ .../image_viewer/image_viewer_context.tsx | 27 ++ .../public/image_viewer/index.tsx | 21 + .../not_found/not_found_light.png | Bin 0 -> 59772 bytes .../not_found/not_found_light@2x.png | Bin 0 -> 160435 bytes .../image_embeddable/public/imports.ts | 28 ++ src/plugins/image_embeddable/public/index.ts | 14 + src/plugins/image_embeddable/public/plugin.ts | 58 +++ src/plugins/image_embeddable/public/types.ts | 31 ++ .../public/utils/validate_image_config.ts | 27 ++ .../public/utils/validate_url.ts | 63 +++ src/plugins/image_embeddable/tsconfig.json | 17 + .../public/custom_time_range_action.tsx | 10 +- .../image_embeddable/elastic_logo.png | Bin 0 -> 34043 bytes .../image_embeddable/image_embeddable.ts | 48 ++ .../image_embeddable/index.ts | 15 + .../apps/dashboard_elements/index.ts | 1 + tsconfig.base.json | 2 + 41 files changed, 1670 insertions(+), 78 deletions(-) create mode 100644 src/plugins/image_embeddable/README.md create mode 100644 src/plugins/image_embeddable/jest.config.js create mode 100644 src/plugins/image_embeddable/kibana.json create mode 100644 src/plugins/image_embeddable/public/image_editor/configure_image.tsx create mode 100644 src/plugins/image_embeddable/public/image_editor/image_editor_flyout.test.tsx create mode 100644 src/plugins/image_embeddable/public/image_editor/image_editor_flyout.tsx create mode 100644 src/plugins/image_embeddable/public/image_editor/index.ts create mode 100644 src/plugins/image_embeddable/public/image_embeddable/image_embeddable.tsx create mode 100644 src/plugins/image_embeddable/public/image_embeddable/image_embeddable_factory.tsx create mode 100644 src/plugins/image_embeddable/public/image_embeddable/index.ts create mode 100644 src/plugins/image_embeddable/public/image_viewer/image_viewer.test.tsx create mode 100644 src/plugins/image_embeddable/public/image_viewer/image_viewer.tsx create mode 100644 src/plugins/image_embeddable/public/image_viewer/image_viewer_context.tsx create mode 100644 src/plugins/image_embeddable/public/image_viewer/index.tsx create mode 100644 src/plugins/image_embeddable/public/image_viewer/not_found/not_found_light.png create mode 100644 src/plugins/image_embeddable/public/image_viewer/not_found/not_found_light@2x.png create mode 100644 src/plugins/image_embeddable/public/imports.ts create mode 100644 src/plugins/image_embeddable/public/index.ts create mode 100644 src/plugins/image_embeddable/public/plugin.ts create mode 100644 src/plugins/image_embeddable/public/types.ts create mode 100644 src/plugins/image_embeddable/public/utils/validate_image_config.ts create mode 100644 src/plugins/image_embeddable/public/utils/validate_url.ts create mode 100644 src/plugins/image_embeddable/tsconfig.json create mode 100644 test/functional/apps/dashboard_elements/image_embeddable/elastic_logo.png create mode 100644 test/functional/apps/dashboard_elements/image_embeddable/image_embeddable.ts create mode 100644 test/functional/apps/dashboard_elements/image_embeddable/index.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d5fc40a4d34fd..783ea220ef5a6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -73,6 +73,7 @@ /src/plugins/ui_actions/ @elastic/kibana-global-experience /src/plugins/ui_actions_enhanced/ @elastic/kibana-global-experience /src/plugins/navigation/ @elastic/kibana-global-experience +/src/plugins/image_embeddable/ @elastic/kibana-global-experience /x-pack/plugins/notifications/ @elastic/kibana-global-experience ## Examples diff --git a/.i18nrc.json b/.i18nrc.json index 84337f9f9fba0..3c95e9b514484 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -53,6 +53,7 @@ "inspectorViews": "src/legacy/core_plugins/inspector_views", "interactiveSetup": "src/plugins/interactive_setup", "interpreter": "src/legacy/core_plugins/interpreter", + "imageEmbeddable": "src/plugins/image_embeddable", "kbn": "src/legacy/core_plugins/kibana", "kbnConfig": "packages/kbn-config/src", "kbnDocViews": "src/legacy/core_plugins/kbn_doc_views", diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 93ffa9e3ff3bc..f726d3b686d82 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -192,6 +192,11 @@ for use in their own application. |Moves the legacy ui/registry/feature_catalogue module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls. +|{kib-repo}blob/{branch}/src/plugins/image_embeddable/README.md[imageEmbeddable] +|This plugin contains image embeddable. Image embeddable allows to embed images into the dashboard. +Images can be added either by URL or by uploading the image file via file service. + + |{kib-repo}blob/{branch}/src/plugins/input_control_vis/README.md[inputControlVis] |Contains the input control visualization allowing to place custom filter controls on a dashboard. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index cab54de0aacc6..fc0b4fc3d0831 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -66,6 +66,7 @@ pageLoadAssetSize: grokdebugger: 26779 guidedOnboarding: 42965 home: 30182 + imageEmbeddable: 12500 indexLifecycleManagement: 107090 indexManagement: 140608 infra: 184320 diff --git a/packages/shared-ux/file/image/impl/src/image.tsx b/packages/shared-ux/file/image/impl/src/image.tsx index 40f9099df9551..e87b46673676a 100644 --- a/packages/shared-ux/file/image/impl/src/image.tsx +++ b/packages/shared-ux/file/image/impl/src/image.tsx @@ -49,7 +49,7 @@ export const Image = ({ src, url, alt, onLoad, onError, meta, ...rest }: Props) return ( - i18n.translate('dashboard.addPanel.savedObjectAddedToContainerSuccessMessageTitle', { - defaultMessage: '{savedObjectName} was added', - values: { - savedObjectName, - }, - }), + getSuccessMessage: (savedObjectName?: string) => + savedObjectName + ? i18n.translate('dashboard.addPanel.savedObjectAddedToContainerSuccessMessageTitle', { + defaultMessage: '{savedObjectName} was added', + values: { + savedObjectName: `'${savedObjectName}'`, + }, + }) + : i18n.translate('dashboard.addPanel.panelAddedToContainerSuccessMessageTitle', { + defaultMessage: 'A panel was added', + }), getNoMatchingObjectsMessage: () => i18n.translate('dashboard.addPanel.noMatchingObjectsMessage', { defaultMessage: 'No matching objects found.', diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx index 72619aa52c0d8..71dd1691400e3 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx @@ -8,6 +8,7 @@ import { EuiHorizontalRule } from '@elastic/eui'; import { METRIC_TYPE } from '@kbn/analytics'; +import { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { AddFromLibraryButton, PrimaryActionButton, @@ -18,6 +19,7 @@ import { import { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public'; import React from 'react'; import { useCallback } from 'react'; +import { dashboardReplacePanelActionStrings } from '../../dashboard_actions/_dashboard_actions_strings'; import { DASHBOARD_APP_ID, DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants'; import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_renderer'; import { pluginServices } from '../../services/plugin_services'; @@ -28,8 +30,9 @@ export function DashboardEditingToolbar() { const { usageCollection, data: { search }, + notifications: { toasts }, settings: { uiSettings }, - embeddable: { getStateTransfer }, + embeddable: { getStateTransfer, getEmbeddableFactory }, visualizations: { get: getVisualization, getAliases: getVisTypeAliases }, } = pluginServices.getServices(); @@ -39,7 +42,13 @@ export function DashboardEditingToolbar() { const IS_DARK_THEME = uiSettings.get('theme:darkMode'); const lensAlias = getVisTypeAliases().find(({ name }) => name === 'lens'); - const quickButtonVisTypes = ['markdown', 'maps']; + const quickButtonVisTypes: Array< + { type: 'vis'; visType: string } | { type: 'embeddable'; embeddableType: string } + > = [ + { type: 'vis', visType: 'markdown' }, + { type: 'embeddable', embeddableType: 'image' }, + { type: 'vis', visType: 'maps' }, + ]; const trackUiMetric = usageCollection.reportUiCounter?.bind( usageCollection, @@ -79,32 +88,77 @@ export function DashboardEditingToolbar() { [stateTransferService, search.session, trackUiMetric] ); - const getVisTypeQuickButton = (visTypeName: string) => { - const visType = - getVisualization(visTypeName) || getVisTypeAliases().find(({ name }) => name === visTypeName); + const createNewEmbeddable = useCallback( + async (embeddableFactory: EmbeddableFactory) => { + if (trackUiMetric) { + trackUiMetric(METRIC_TYPE.CLICK, embeddableFactory.type); + } + + let explicitInput: Awaited>; + try { + explicitInput = await embeddableFactory.getExplicitInput(); + } catch (e) { + // error likely means user canceled embeddable creation + return; + } - if (visType) { - if ('aliasPath' in visType) { - const { name, icon, title } = visType as VisTypeAlias; + const newEmbeddable = await dashboardContainer.addNewEmbeddable( + embeddableFactory.type, + explicitInput + ); - return { - iconType: icon, - createType: title, - onClick: createNewVisType(visType as VisTypeAlias), - 'data-test-subj': `dashboardQuickButton${name}`, - }; - } else { - const { name, icon, title, titleInWizard } = visType as BaseVisType; - - return { - iconType: icon, - createType: titleInWizard || title, - onClick: createNewVisType(visType as BaseVisType), - 'data-test-subj': `dashboardQuickButton${name}`, - }; + if (newEmbeddable) { + toasts.addSuccess({ + title: dashboardReplacePanelActionStrings.getSuccessMessage(newEmbeddable.getTitle()), + 'data-test-subj': 'addEmbeddableToDashboardSuccess', + }); } + }, + [trackUiMetric, dashboardContainer, toasts] + ); + + const getVisTypeQuickButton = (quickButtonForType: typeof quickButtonVisTypes[0]) => { + if (quickButtonForType.type === 'vis') { + const visTypeName = quickButtonForType.visType; + const visType = + getVisualization(visTypeName) || + getVisTypeAliases().find(({ name }) => name === visTypeName); + + if (visType) { + if ('aliasPath' in visType) { + const { name, icon, title } = visType as VisTypeAlias; + + return { + iconType: icon, + createType: title, + onClick: createNewVisType(visType as VisTypeAlias), + 'data-test-subj': `dashboardQuickButton${name}`, + }; + } else { + const { name, icon, title, titleInWizard } = visType as BaseVisType; + + return { + iconType: icon, + createType: titleInWizard || title, + onClick: createNewVisType(visType as BaseVisType), + 'data-test-subj': `dashboardQuickButton${name}`, + }; + } + } + } else { + const embeddableType = quickButtonForType.embeddableType; + const embeddableFactory = getEmbeddableFactory(embeddableType); + return { + iconType: embeddableFactory?.getIconType(), + createType: embeddableFactory?.getDisplayName(), + onClick: () => { + if (embeddableFactory) { + createNewEmbeddable(embeddableFactory); + } + }, + 'data-test-subj': `dashboardQuickButton${embeddableType}`, + }; } - return; }; const quickButtons = quickButtonVisTypes @@ -127,7 +181,10 @@ export function DashboardEditingToolbar() { ), quickButtonGroup: , extraButtons: [ - , + , dashboardContainer.addFromLibrary()} data-test-subj="dashboardAddPanelButton" diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx index ae338145915c6..5de32e3b10019 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx @@ -8,31 +8,25 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { + EuiBadge, EuiContextMenu, - EuiContextMenuPanelItemDescriptor, EuiContextMenuItemIcon, + EuiContextMenuPanelItemDescriptor, EuiFlexGroup, EuiFlexItem, - EuiBadge, } from '@elastic/eui'; -import { METRIC_TYPE } from '@kbn/analytics'; import { i18n } from '@kbn/i18n'; import { type BaseVisType, VisGroups, type VisTypeAlias } from '@kbn/visualizations-plugin/public'; import { SolutionToolbarPopover } from '@kbn/presentation-util-plugin/public'; -import type { - EmbeddableFactory, - EmbeddableFactoryDefinition, - EmbeddableInput, -} from '@kbn/embeddable-plugin/public'; - +import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { pluginServices } from '../../services/plugin_services'; -import { getPanelAddedSuccessString } from '../_dashboard_app_strings'; -import { DASHBOARD_APP_ID, DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants'; -import { useDashboardContainerContext } from '../../dashboard_container/dashboard_container_renderer'; +import { DASHBOARD_APP_ID } from '../../dashboard_constants'; interface Props { /** Handler for creating new visualization of a specified type */ createNewVisType: (visType: BaseVisType | VisTypeAlias) => () => void; + /** Handler for creating a new embeddable of a specified type */ + createNewEmbeddable: (embeddableFactory: EmbeddableFactory) => void; } interface FactoryGroup { @@ -40,7 +34,7 @@ interface FactoryGroup { appName: string; icon: EuiContextMenuItemIcon; panelId: number; - factories: EmbeddableFactoryDefinition[]; + factories: EmbeddableFactory[]; } interface UnwrappedEmbeddableFactory { @@ -48,12 +42,10 @@ interface UnwrappedEmbeddableFactory { isEditable: boolean; } -export const EditorMenu = ({ createNewVisType }: Props) => { +export const EditorMenu = ({ createNewVisType, createNewEmbeddable }: Props) => { const { embeddable, - notifications: { toasts }, settings: { uiSettings }, - usageCollection, visualizations: { getAliases: getVisTypeAliases, getByGroup: getVisTypesByGroup, @@ -61,8 +53,6 @@ export const EditorMenu = ({ createNewVisType }: Props) => { }, } = pluginServices.getServices(); - const { embeddableInstance: dashboardContainer } = useDashboardContainerContext(); - const embeddableFactories = useMemo( () => Array.from(embeddable.getEmbeddableFactories()), [embeddable] @@ -84,11 +74,6 @@ export const EditorMenu = ({ createNewVisType }: Props) => { const LABS_ENABLED = uiSettings.get('visualize:enableLabs'); - const trackUiMetric = usageCollection.reportUiCounter?.bind( - usageCollection, - DASHBOARD_UI_METRIC_ID - ); - const createNewAggsBasedVis = useCallback( (visType?: BaseVisType) => () => showNewVisModal({ @@ -129,7 +114,7 @@ export const EditorMenu = ({ createNewVisType }: Props) => { ); const factoryGroupMap: Record = {}; - const ungroupedFactories: EmbeddableFactoryDefinition[] = []; + const ungroupedFactories: EmbeddableFactory[] = []; const aggBasedPanelID = 1; let panelCount = 1 + aggBasedPanelID; @@ -211,7 +196,7 @@ export const EditorMenu = ({ createNewVisType }: Props) => { }; const getEmbeddableFactoryMenuItem = ( - factory: EmbeddableFactoryDefinition, + factory: EmbeddableFactory, closePopover: () => void ): EuiContextMenuPanelItemDescriptor => { const icon = factory?.getIconType ? factory.getIconType() : 'empty'; @@ -224,23 +209,7 @@ export const EditorMenu = ({ createNewVisType }: Props) => { toolTipContent, onClick: async () => { closePopover(); - if (trackUiMetric) { - trackUiMetric(METRIC_TYPE.CLICK, factory.type); - } - let newEmbeddable; - if (factory.getExplicitInput) { - const explicitInput = await factory.getExplicitInput(); - newEmbeddable = await dashboardContainer.addNewEmbeddable(factory.type, explicitInput); - } else { - newEmbeddable = await factory.create({} as EmbeddableInput, dashboardContainer); - } - - if (newEmbeddable) { - toasts.addSuccess({ - title: getPanelAddedSuccessString(`'${newEmbeddable.getInput().title}'` || ''), - 'data-test-subj': 'addEmbeddableToDashboardSuccess', - }); - } + createNewEmbeddable(factory); }, 'data-test-subj': `createNew-${factory.type}`, }; diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index 4bfa26add28ba..1dcecf4ac894d 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -75,13 +75,29 @@ export class EditPanelAction implements Action { embeddable && embeddable.getOutput().editable && (embeddable.getOutput().editUrl || - (embeddable.getOutput().editApp && embeddable.getOutput().editPath)) + (embeddable.getOutput().editApp && embeddable.getOutput().editPath) || + embeddable.getOutput().editableWithExplicitInput) ); const inDashboardEditMode = embeddable.getInput().viewMode === ViewMode.EDIT; return Boolean(canEditEmbeddable && inDashboardEditMode); } public async execute(context: ActionContext) { + const embeddable = context.embeddable; + const { editableWithExplicitInput } = embeddable.getOutput(); + + if (editableWithExplicitInput) { + const factory = this.getEmbeddableFactory(embeddable.type); + if (!factory) { + throw new EmbeddableFactoryNotFoundError(embeddable.type); + } + + const oldExplicitInput = embeddable.getExplicitInput(); + const newExplicitInput = await factory.getExplicitInput(oldExplicitInput); + embeddable.parent?.replaceEmbeddable(embeddable.id, newExplicitInput); + return; + } + const appTarget = this.getAppTarget(context); if (appTarget) { if (this.stateTransfer && appTarget.state) { diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts index 317ff4f773f23..3e1036b0813d8 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts @@ -96,8 +96,10 @@ export interface EmbeddableFactory< * Can be used to request explicit input from the user, to be passed in to `EmbeddableFactory:create`. * Explicit input is stored on the parent container for this embeddable. It overrides all inherited * input passed down from the parent container. + * + * Can be used to edit an embeddable by re-requesting explicit input. Initial input can be provided to allow the editor to show the current state. */ - getExplicitInput(): Promise>; + getExplicitInput(initialInput?: Partial): Promise>; /** * Creates a new embeddable instance based off the saved object id. diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts index 1d3cc7980ad62..2370b62743466 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -28,6 +28,8 @@ export interface EmbeddableOutput { defaultTitle?: string; title?: string; editable?: boolean; + // Whether the embeddable can be edited inline by re-requesting the explicit input from the user + editableWithExplicitInput?: boolean; savedObjectId?: string; } diff --git a/src/plugins/files/kibana.json b/src/plugins/files/kibana.json index 47637d78fa0de..9c8a096bd63e4 100755 --- a/src/plugins/files/kibana.json +++ b/src/plugins/files/kibana.json @@ -9,6 +9,7 @@ "description": "File upload, download, sharing, and serving over HTTP implementation in Kibana.", "server": true, "ui": true, + "extraPublicDirs": ["common"], "requiredBundles": ["kibanaUtils"], "optionalPlugins": ["security", "usageCollection"] } diff --git a/src/plugins/image_embeddable/README.md b/src/plugins/image_embeddable/README.md new file mode 100644 index 0000000000000..44fd0db898b16 --- /dev/null +++ b/src/plugins/image_embeddable/README.md @@ -0,0 +1,4 @@ +## Image Embeddable + +This plugin contains image embeddable. Image embeddable allows to embed images into the dashboard. +Images can be added either by URL or by uploading the image file via file service. diff --git a/src/plugins/image_embeddable/jest.config.js b/src/plugins/image_embeddable/jest.config.js new file mode 100644 index 0000000000000..1a7b6693a4889 --- /dev/null +++ b/src/plugins/image_embeddable/jest.config.js @@ -0,0 +1,18 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/image_embeddable'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/image_embeddable', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/image_embeddable/{__packages_do_not_import__,common,public,server,static}/**/*.{ts,tsx}', + ], +}; diff --git a/src/plugins/image_embeddable/kibana.json b/src/plugins/image_embeddable/kibana.json new file mode 100644 index 0000000000000..ba79a2158425a --- /dev/null +++ b/src/plugins/image_embeddable/kibana.json @@ -0,0 +1,13 @@ +{ + "id": "imageEmbeddable", + "version": "kibana", + "server": false, + "ui": true, + "owner": { + "name": "@elastic/kibana-global-experience", + "githubTeam": "@elastic/kibana-global-experience" + }, + "description": "Image embeddable", + "requiredPlugins": ["embeddable", "files"], + "requiredBundles": ["kibanaUtils", "kibanaReact"] +} diff --git a/src/plugins/image_embeddable/public/image_editor/configure_image.tsx b/src/plugins/image_embeddable/public/image_editor/configure_image.tsx new file mode 100644 index 0000000000000..f5b20ce1ec347 --- /dev/null +++ b/src/plugins/image_embeddable/public/image_editor/configure_image.tsx @@ -0,0 +1,87 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { FilesContext } from '@kbn/shared-ux-file-context'; +import { skip, take, takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { ImageConfig } from '../types'; +import { ImageEditorFlyout } from './image_editor_flyout'; +import { ImageViewerContext } from '../image_viewer'; +import { + OverlayStart, + ApplicationStart, + FilesClient, + FileImageMetadata, + ThemeServiceStart, +} from '../imports'; +import { ValidateUrlFn } from '../utils/validate_url'; + +/** + * @throws in case user cancels + */ +export async function configureImage( + deps: { + files: FilesClient; + overlays: OverlayStart; + theme: ThemeServiceStart; + currentAppId$: ApplicationStart['currentAppId$']; + validateUrl: ValidateUrlFn; + getImageDownloadHref: (fileId: string) => string; + }, + initialImageConfig?: ImageConfig +): Promise { + return new Promise((resolve, reject) => { + const closed$ = new Subject(); + + const onSave = (imageConfig: ImageConfig) => { + resolve(imageConfig); + handle.close(); + }; + + const onCancel = () => { + reject(); + handle.close(); + }; + + // Close the flyout on application change. + deps.currentAppId$.pipe(takeUntil(closed$), skip(1), take(1)).subscribe(() => { + handle.close(); + }); + + const handle = deps.overlays.openFlyout( + toMountPoint( + + + + + , + { theme$: deps.theme.theme$ } + ), + { + ownFocus: true, + 'data-test-subj': 'createImageEmbeddableFlyout', + } + ); + + handle.onClose.then(() => { + closed$.next(true); + }); + }); +} diff --git a/src/plugins/image_embeddable/public/image_editor/image_editor_flyout.test.tsx b/src/plugins/image_embeddable/public/image_editor/image_editor_flyout.test.tsx new file mode 100644 index 0000000000000..b64652139a78a --- /dev/null +++ b/src/plugins/image_embeddable/public/image_editor/image_editor_flyout.test.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { I18nProvider } from '@kbn/i18n-react'; +import { FilesContext } from '@kbn/shared-ux-file-context'; +import { createMockFilesClient } from '@kbn/shared-ux-file-mocks'; +import { ImageViewerContext } from '../image_viewer'; +import { ImageEditorFlyout, ImageEditorFlyoutProps } from './image_editor_flyout'; +import { imageEmbeddableFileKind } from '../imports'; + +const validateUrl = jest.fn(() => ({ isValid: true })); + +beforeEach(() => { + validateUrl.mockImplementation(() => ({ isValid: true })); +}); + +const filesClient = createMockFilesClient(); +filesClient.getFileKind.mockImplementation(() => imageEmbeddableFileKind); + +const ImageEditor = (props: Partial) => { + return ( + + + `https://elastic.co/${fileId}`, + validateUrl, + }} + > + {}} + onSave={() => {}} + {...props} + /> + + + + ); +}; + +test('should call onCancel when "Close" clicked', async () => { + const onCancel = jest.fn(); + const { getByText } = render(); + expect(getByText('Close')).toBeVisible(); + await userEvent.click(getByText('Close')); + expect(onCancel).toBeCalled(); +}); + +test('should call onSave when "Save" clicked (url)', async () => { + const onSave = jest.fn(); + const { getByText, getByTestId } = render(); + + await userEvent.click(getByText('Use link')); + await userEvent.type(getByTestId(`imageEmbeddableEditorUrlInput`), `https://elastic.co/image`); + await userEvent.type(getByTestId(`imageEmbeddableEditorAltInput`), `alt text`); + + expect(getByTestId(`imageEmbeddableEditorSave`)).toBeVisible(); + await userEvent.click(getByTestId(`imageEmbeddableEditorSave`)); + expect(onSave).toBeCalledWith({ + altText: 'alt text', + backgroundColor: '', + sizing: { + objectFit: 'contain', + }, + src: { + type: 'url', + url: 'https://elastic.co/image', + }, + }); +}); + +test('should be able to edit', async () => { + const initialImageConfig = { + altText: 'alt text', + backgroundColor: '', + sizing: { + objectFit: 'contain' as const, + }, + src: { + type: 'url' as const, + url: 'https://elastic.co/image', + }, + }; + const onSave = jest.fn(); + const { getByTestId } = render( + + ); + + expect(getByTestId(`imageEmbeddableEditorUrlInput`)).toHaveValue('https://elastic.co/image'); + + await userEvent.type(getByTestId(`imageEmbeddableEditorUrlInput`), `-changed`); + await userEvent.type(getByTestId(`imageEmbeddableEditorAltInput`), ` changed`); + + expect(getByTestId(`imageEmbeddableEditorSave`)).toBeVisible(); + await userEvent.click(getByTestId(`imageEmbeddableEditorSave`)); + expect(onSave).toBeCalledWith({ + altText: 'alt text changed', + backgroundColor: '', + sizing: { + objectFit: 'contain', + }, + src: { + type: 'url', + url: 'https://elastic.co/image-changed', + }, + }); +}); + +test(`shouldn't be able to save if url is invalid`, async () => { + const initialImageConfig = { + altText: 'alt text', + backgroundColor: '', + sizing: { + objectFit: 'contain' as const, + }, + src: { + type: 'url' as const, + url: 'https://elastic.co/image', + }, + }; + + validateUrl.mockImplementation(() => ({ isValid: false, error: 'error' })); + + const { getByTestId } = render(); + + expect(getByTestId(`imageEmbeddableEditorSave`)).toBeDisabled(); +}); diff --git a/src/plugins/image_embeddable/public/image_editor/image_editor_flyout.tsx b/src/plugins/image_embeddable/public/image_editor/image_editor_flyout.tsx new file mode 100644 index 0000000000000..c3a9cfc447fed --- /dev/null +++ b/src/plugins/image_embeddable/public/image_editor/image_editor_flyout.tsx @@ -0,0 +1,445 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiTab, + EuiTabs, + EuiTitle, + EuiSpacer, + EuiLink, + EuiEmptyPrompt, + EuiTextArea, + EuiFormRow, + EuiSelect, + EuiColorPicker, + useColorPickerState, + EuiLoadingSpinner, + useEuiTheme, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { css } from '@emotion/react'; +import { FileUpload } from '@kbn/shared-ux-file-upload'; +import { FilePicker } from '@kbn/shared-ux-file-picker'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { FileImageMetadata, imageEmbeddableFileKind } from '../imports'; +import { ImageConfig } from '../types'; +import { ImageViewer } from '../image_viewer/image_viewer'; // use eager version to avoid flickering +import { ValidateUrlFn } from '../utils/validate_url'; +import { validateImageConfig, DraftImageConfig } from '../utils/validate_image_config'; + +/** + * Shared sizing css for image, upload placeholder, empty and not found state + * Makes sure the container has not too large height to preserve vertical space for the image configuration in the flyout + */ +const CONTAINER_SIZING_CSS = css({ + aspectRatio: `21 / 9`, + width: `100%`, + height: `auto`, + maxHeight: `max(20vh, 180px)`, +}); + +export interface ImageEditorFlyoutProps { + onCancel: () => void; + onSave: (imageConfig: ImageConfig) => void; + initialImageConfig?: ImageConfig; + validateUrl: ValidateUrlFn; +} + +export function ImageEditorFlyout(props: ImageEditorFlyoutProps) { + const isEditing = !!props.initialImageConfig; + const { euiTheme } = useEuiTheme(); + const [fileId, setFileId] = useState(() => + props.initialImageConfig?.src?.type === 'file' ? props.initialImageConfig.src.fileId : undefined + ); + const [fileImageMeta, setFileImageMeta] = useState(() => + props.initialImageConfig?.src?.type === 'file' + ? props.initialImageConfig.src.fileImageMeta + : undefined + ); + const [srcType, setSrcType] = useState( + () => props.initialImageConfig?.src?.type ?? 'file' + ); + const [srcUrl, setSrcUrl] = useState(() => + props.initialImageConfig?.src?.type === 'url' ? props.initialImageConfig.src.url : '' + ); + const [srcUrlError, setSrcUrlError] = useState(() => { + if (srcUrl) return props.validateUrl(srcUrl)?.error ?? null; + return null; + }); + const [isFilePickerOpen, setIsFilePickerOpen] = useState(false); + const [sizingObjectFit, setSizingObjectFit] = useState( + () => props.initialImageConfig?.sizing?.objectFit ?? 'contain' + ); + const [altText, setAltText] = useState(() => props.initialImageConfig?.altText ?? ''); + const [color, setColor, colorErrors] = useColorPickerState( + props?.initialImageConfig?.backgroundColor + ); + const isColorInvalid = !!color && !!colorErrors; + + const draftImageConfig: DraftImageConfig = { + ...props.initialImageConfig, + src: + srcType === 'url' + ? { + type: 'url', + url: srcUrl, + } + : { type: 'file', fileId, fileImageMeta }, + altText, + backgroundColor: colorErrors ? undefined : color, + sizing: { + objectFit: sizingObjectFit, + }, + }; + + const isDraftImageConfigValid = validateImageConfig(draftImageConfig, { + validateUrl: props.validateUrl, + }); + + const onSave = () => { + if (!isDraftImageConfigValid) return; + props.onSave(draftImageConfig); + }; + + return ( + <> + + +

+ {isEditing ? ( + + ) : ( + + )} +

+ + + + setSrcType('file')} isSelected={srcType === 'file'}> + + + setSrcType('url')} isSelected={srcType === 'url'}> + + + + + + {srcType === 'file' && ( + <> + {isDraftImageConfigValid ? ( + setIsFilePickerOpen(true)} + onClear={() => { + setFileId(undefined); + setFileImageMeta(undefined); + }} + containerCSS={css` + border: ${euiTheme.border.thin}; + background-color: ${euiTheme.colors.lightestShade}; + `} + /> + ) : ( + + <> + setFileId(files[0]?.id)} + immediate={true} + initialPromptText={i18n.translate( + 'imageEmbeddable.imageEditor.uploadImagePromptText', + { + defaultMessage: 'Select or drag and drop an image', + } + )} + fullWidth={true} + lazyLoadFallback={ +
+ +
+ } + /> +

+ setIsFilePickerOpen(true)} + data-test-subj="imageEmbeddableEditorSelectFiles" + > + + +

+ +
+ )} + + )} + {srcType === 'url' && ( + <> + {!isDraftImageConfigValid ? ( + + +

+ } + titleSize={'s'} + /> + ) : ( + { + setSrcUrlError( + i18n.translate('imageEmbeddable.imageEditor.urlFailedToLoadImageErrorMessage', { + defaultMessage: 'Unable to load image.', + }) + ); + }} + containerCSS={css` + border: ${euiTheme.border.thin}; + background-color: ${euiTheme.colors.lightestShade}; + `} + /> + )} + + + + } + helpText={ + + } + fullWidth={true} + isInvalid={!!srcUrlError} + error={srcUrlError} + > + { + const url = e.target.value; + + const { isValid, error } = props.validateUrl(url); + if (!isValid) { + setSrcUrlError(error!); + } else { + setSrcUrlError(null); + } + + setSrcUrl(e.target.value); + }} + /> + + + )} + + + } + fullWidth + > + + setSizingObjectFit(e.target.value as ImageConfig['sizing']['objectFit']) + } + /> + + + + + } + fullWidth + isInvalid={isColorInvalid} + error={colorErrors} + > + + + + + + } + fullWidth + > + { + setAltText(e.target.value); + }} + /> + +
+ + + + + + + + + + + + + + + + {isFilePickerOpen && ( + { + setIsFilePickerOpen(false); + }} + onDone={([file]) => { + setFileId(file.id); + setFileImageMeta(file.meta as FileImageMetadata); + setIsFilePickerOpen(false); + }} + /> + )} + + ); +} diff --git a/src/plugins/image_embeddable/public/image_editor/index.ts b/src/plugins/image_embeddable/public/image_editor/index.ts new file mode 100644 index 0000000000000..093c019c748d8 --- /dev/null +++ b/src/plugins/image_embeddable/public/image_editor/index.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './configure_image'; diff --git a/src/plugins/image_embeddable/public/image_embeddable/image_embeddable.tsx b/src/plugins/image_embeddable/public/image_embeddable/image_embeddable.tsx new file mode 100644 index 0000000000000..bf5c90e9721e3 --- /dev/null +++ b/src/plugins/image_embeddable/public/image_embeddable/image_embeddable.tsx @@ -0,0 +1,70 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public'; +import { ImageEmbeddableInput } from './image_embeddable_factory'; +import { ImageViewer, ImageViewerContext } from '../image_viewer'; +import { createValidateUrl } from '../utils/validate_url'; + +export const IMAGE_EMBEDDABLE_TYPE = 'image'; + +export class ImageEmbeddable extends Embeddable { + public readonly type = IMAGE_EMBEDDABLE_TYPE; + + constructor( + private deps: { + getImageDownloadHref: (fileId: string) => string; + validateUrl: ReturnType; + }, + initialInput: ImageEmbeddableInput, + parent?: IContainer + ) { + super( + initialInput, + { + editable: true, + editableWithExplicitInput: true, + }, + parent + ); + } + + public render(el: HTMLElement) { + super.render(el); // calling super.render initializes renderComplete and setTitle + el.setAttribute('data-shared-item', ''); + const ImageEmbeddableViewer = this.ImageEmbeddableViewer; + return ; + } + + public reload() {} + + private ImageEmbeddableViewer = (props: { embeddable: ImageEmbeddable }) => { + const input = useObservable(props.embeddable.getInput$(), props.embeddable.getInput()); + + return ( + + { + this.renderComplete.dispatchComplete(); + }} + onError={() => { + this.renderComplete.dispatchError(); + }} + /> + + ); + }; +} diff --git a/src/plugins/image_embeddable/public/image_embeddable/image_embeddable_factory.tsx b/src/plugins/image_embeddable/public/image_embeddable/image_embeddable_factory.tsx new file mode 100644 index 0000000000000..4806c41422c21 --- /dev/null +++ b/src/plugins/image_embeddable/public/image_embeddable/image_embeddable_factory.tsx @@ -0,0 +1,92 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { IExternalUrl } from '@kbn/core-http-browser'; +import { + IContainer, + EmbeddableInput, + EmbeddableFactoryDefinition, + ApplicationStart, + OverlayStart, + FilesClient, + FileImageMetadata, + imageEmbeddableFileKind, + ThemeServiceStart, +} from '../imports'; +import { ImageEmbeddable, IMAGE_EMBEDDABLE_TYPE } from './image_embeddable'; +import { ImageConfig } from '../types'; +import { createValidateUrl } from '../utils/validate_url'; + +export interface ImageEmbeddableFactoryDeps { + start: () => { + application: ApplicationStart; + overlays: OverlayStart; + files: FilesClient; + externalUrl: IExternalUrl; + theme: ThemeServiceStart; + }; +} + +export interface ImageEmbeddableInput extends EmbeddableInput { + imageConfig: ImageConfig; +} + +export class ImageEmbeddableFactoryDefinition + implements EmbeddableFactoryDefinition +{ + public readonly type = IMAGE_EMBEDDABLE_TYPE; + + constructor(private deps: ImageEmbeddableFactoryDeps) {} + + public async isEditable() { + return Boolean(this.deps.start().application.capabilities.dashboard?.showWriteControls); + } + + public async create(initialInput: ImageEmbeddableInput, parent?: IContainer) { + return new ImageEmbeddable( + { + getImageDownloadHref: this.getImageDownloadHref, + validateUrl: createValidateUrl(this.deps.start().externalUrl), + }, + initialInput, + parent + ); + } + + public getDisplayName() { + return i18n.translate('imageEmbeddable.imageEmbeddableFactory.displayName', { + defaultMessage: 'Image', + }); + } + + public getIconType() { + return `image`; + } + + public async getExplicitInput(initialInput: ImageEmbeddableInput) { + const { configureImage } = await import('../image_editor'); + + const imageConfig = await configureImage( + { + files: this.deps.start().files, + overlays: this.deps.start().overlays, + currentAppId$: this.deps.start().application.currentAppId$, + validateUrl: createValidateUrl(this.deps.start().externalUrl), + getImageDownloadHref: this.getImageDownloadHref, + theme: this.deps.start().theme, + }, + initialInput ? initialInput.imageConfig : undefined + ); + + return { imageConfig }; + } + + private getImageDownloadHref = (fileId: string) => + this.deps.start().files.getDownloadHref({ id: fileId, fileKind: imageEmbeddableFileKind.id }); +} diff --git a/src/plugins/image_embeddable/public/image_embeddable/index.ts b/src/plugins/image_embeddable/public/image_embeddable/index.ts new file mode 100644 index 0000000000000..83daaeeae07d0 --- /dev/null +++ b/src/plugins/image_embeddable/public/image_embeddable/index.ts @@ -0,0 +1,10 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './image_embeddable'; +export * from './image_embeddable_factory'; diff --git a/src/plugins/image_embeddable/public/image_viewer/image_viewer.test.tsx b/src/plugins/image_embeddable/public/image_viewer/image_viewer.test.tsx new file mode 100644 index 0000000000000..f57685f861dbc --- /dev/null +++ b/src/plugins/image_embeddable/public/image_viewer/image_viewer.test.tsx @@ -0,0 +1,77 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { ImageViewer } from './image_viewer'; +import { ImageViewerContext } from './image_viewer_context'; +import { ImageConfig } from '../types'; + +const validateUrl = jest.fn(() => ({ isValid: true })); + +beforeEach(() => { + validateUrl.mockImplementation(() => ({ isValid: true })); +}); + +const DefaultImageViewer = (props: { imageConfig: ImageConfig }) => { + return ( + `https://elastic.co/${fileId}`, + validateUrl, + }} + > + + + ); +}; + +test('should display an image by a valid url', () => { + const { getByAltText } = render( + + ); + + expect(getByAltText(`alt text`)).toBeVisible(); +}); + +test('should display a 404 if url is invalid', () => { + validateUrl.mockImplementation(() => ({ isValid: false })); + const { queryByAltText, getByTestId } = render( + + ); + + expect(queryByAltText(`alt text`)).toBeNull(); + expect(getByTestId(`imageNotFound`)).toBeVisible(); +}); + +test('should display an image by file id', () => { + const { getByAltText } = render( + + ); + + expect(getByAltText(`alt text`)).toBeVisible(); + expect(getByAltText(`alt text`)).toHaveAttribute('src', 'https://elastic.co/imageId'); +}); diff --git a/src/plugins/image_embeddable/public/image_viewer/image_viewer.tsx b/src/plugins/image_embeddable/public/image_viewer/image_viewer.tsx new file mode 100644 index 0000000000000..4ee6b0d6d9364 --- /dev/null +++ b/src/plugins/image_embeddable/public/image_viewer/image_viewer.tsx @@ -0,0 +1,212 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect, useState } from 'react'; +import { css, SerializedStyles } from '@emotion/react'; +import { FileImage } from '@kbn/shared-ux-file-image'; +import classNames from 'classnames'; +import { + EuiButtonIcon, + EuiEmptyPrompt, + EuiImage, + useEuiTheme, + useResizeObserver, + useIsWithinBreakpoints, + EuiImageProps, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { ImageConfig } from '../types'; +import notFound from './not_found/not_found_light.png'; +import notFound2x from './not_found/not_found_light@2x.png'; +import { validateImageConfig } from '../utils/validate_image_config'; +import { useImageViewerContext } from './image_viewer_context'; + +export interface ImageViewerProps { + imageConfig: ImageConfig; + className?: string; + onChange?: () => void; + onClear?: () => void; + onError?: () => void; + onLoad?: () => void; + containerCSS?: SerializedStyles; +} + +export function ImageViewer({ + imageConfig, + onChange, + onClear, + onError, + onLoad, + className, + containerCSS, +}: ImageViewerProps) { + const { euiTheme } = useEuiTheme(); + const { getImageDownloadHref, validateUrl } = useImageViewerContext(); + + const isImageConfigValid = validateImageConfig(imageConfig, { validateUrl }); + + const src = + imageConfig.src.type === 'url' + ? imageConfig.src.url + : getImageDownloadHref(imageConfig.src.fileId); + + const [hasFailedToLoad, setFailedToLoad] = useState(false); + + useEffect(() => { + setFailedToLoad(false); + }, [src]); + + return ( +
+ {(hasFailedToLoad || !isImageConfigValid) && } + {isImageConfigValid && ( + { + if (onChange) onChange(); + }} + onLoad={() => { + if (onLoad) onLoad(); + }} + onError={() => { + setFailedToLoad(true); + if (onError) onError(); + }} + /> + )} + {onClear && ( + { + if (onClear) onClear(); + }} + /> + )} +
+ ); +} + +function NotFound() { + const [resizeRef, setRef] = React.useState(null); + const isLargeScreen = useIsWithinBreakpoints(['l', 'xl'], true); + const dimensions = useResizeObserver(resizeRef); + let mode: 'none' | 'only-image' | 'image-and-text' = 'none'; + if (!resizeRef) { + mode = 'none'; + } else if (dimensions.height > 200 && dimensions.width > 320 && isLargeScreen) { + mode = 'image-and-text'; + } else { + mode = 'only-image'; + } + + return ( +
setRef(node)} + css={css` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + .euiPanel, + .euiEmptyPrompt__main { + height: 100%; + width: 100%; + max-width: none; + } + `} + > + {mode === 'only-image' && ( + + )} + {mode === 'image-and-text' && ( + } + title={ +

+ +

+ } + layout="horizontal" + body={ +

+ +

+ } + /> + )} +
+ ); +} + +const NotFoundImage = React.memo((props: Partial>) => ( + +)); diff --git a/src/plugins/image_embeddable/public/image_viewer/image_viewer_context.tsx b/src/plugins/image_embeddable/public/image_viewer/image_viewer_context.tsx new file mode 100644 index 0000000000000..ac75142c360b3 --- /dev/null +++ b/src/plugins/image_embeddable/public/image_viewer/image_viewer_context.tsx @@ -0,0 +1,27 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createContext, useContext } from 'react'; +import type { createValidateUrl } from '../utils/validate_url'; + +export interface ImageViewerContextValue { + getImageDownloadHref: (fileId: string) => string; + validateUrl: ReturnType; +} + +export const ImageViewerContext = createContext( + null as unknown as ImageViewerContextValue +); + +export const useImageViewerContext = () => { + const ctx = useContext(ImageViewerContext); + if (!ctx) { + throw new Error('ImageViewerContext is not found!'); + } + return ctx; +}; diff --git a/src/plugins/image_embeddable/public/image_viewer/index.tsx b/src/plugins/image_embeddable/public/image_viewer/index.tsx new file mode 100644 index 0000000000000..82570558765ad --- /dev/null +++ b/src/plugins/image_embeddable/public/image_viewer/index.tsx @@ -0,0 +1,21 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +export { ImageViewerContext, type ImageViewerContextValue } from './image_viewer_context'; +import type { ImageViewerProps } from './image_viewer'; + +const LazyImageViewer = React.lazy(() => + import('./image_viewer').then((m) => ({ default: m.ImageViewer })) +); +export const ImageViewer = (props: ImageViewerProps) => ( + }> + + +); diff --git a/src/plugins/image_embeddable/public/image_viewer/not_found/not_found_light.png b/src/plugins/image_embeddable/public/image_viewer/not_found/not_found_light.png new file mode 100644 index 0000000000000000000000000000000000000000..695c7657d55d64f05cfd1ff6937b74bea496c0e9 GIT binary patch literal 59772 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

Sa7yf6h*Iov_ueZ=^mRzFA4&*4y#{5 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%0D}6xa}eH67WWOFc6aBeCddAzl?HXsd=p&3(? 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 j8JC)8H8(liddS}+?)d^#oJ(sOrf?5Uf zYp%C5p>?bu;W1W0Nl69M+;jTSlEb8?HwAr;;pY`I0iPM{?}n2_)Rm#F_?EmS{mhLN zB3Y3e{W0{7j^@at+6e1E-xvgMKZwp>ndj3(S4zglgcRec&N;|tQ$Q2Ql;5+em|k9; zUpehE@fEAQTR*0vOz%&xj=1QzSy%q9_8r0tt&)N&_Tb->(*gT4w_~as{NQWCjKvLl z&HlhN#Qw4!!k;*nanZ>vb+k4Up2bb*V|_bWUJz%6xLk>(pmjpo5t&4_&{HVHUmGv} zf|Mw_XpMPKL>o2OKYCi_8opY)RF?H^4h8%n!iSJMJ8;S4uX%lc)>kdP-9FaBNy1nJfKA&CKHvspV!p9My)2 z&&3R9Pl9p9hiChz29!5|^2g1D#XQ6(+M@0utLxInspsa{buq#I%~3Cwbq-fop}3eg zxg-|k9fcCyRieV=X52GVy7|`_39FfW8odpFZ{K@*1X9+p4b-)7x*ki4c!m{x_ti1j#WI zz!a_N(#tGTEt$Sl`Iu5}&6(nIc)QQ@H!=d{E0rlaT@c+A$;V1J6+DZPVV2jZ&{2K` zUm&`HzJi8tdo64A47xx%dO2 zR3qf=Wdh(KEW1rV!`<#-?UpqsAEwyi*tKi5*#CdCw|*F>K8VSI|~U=wVCnPWb2}-|U`sHoJpX!R7b*^uTVH*k_;w~r_yM>cZhdz(>b)4wXrH1`UdG&o z^0#-9bjQ*Vk+zlPIWOk;|z|cK(DnGaknYE)# zsZ!gA3@LAj`*JL9Abm#u?mrarAz`Zp+);t9LiMgx)sJh_aa<}y>W@W$Y}#}!(GLHr zN?{AY#APA$6cWo~Q|`;lkE*rnjKP+Bu6db-7`6|%ikv0E9fzmnvGYM4htGM9LGthU z!i`_p}vVaL>)H&{};xZs=foZOZSJy#NY1U;D-NQw5)OgYc*!qtnO zr*JcNJQrBdp7QjA5G$+bsHdmvn%Tv?fbucid3lW{l)*x3c3bi*tD6u5rfD<_HDz?2 zl|RkBEMIM*YU=03S&$a4^4GjkhmLy>u-73|zU1qX8YI>@_3#O)v^Y6NN^`-Ex)`5v z-5pwkOHvqGK~e;guok`9eS{F4QMDYty<74sd|%->5-AM}d}XNQsO<-(sQ;TO?5q$?l{5Cdl^zEpkvX-+_S|!AaLBc={DKwn#fZ!hL9){MWXZ741hEg7G z3R~k_*tu>j+4t;CE)~ovm~`Go*+_x9&NA0%)rwT8GQP5iW==R&sFPtc9vo_F(HdHc z?pi{xcZp~xMWgdHhhZq{R@E_!$EhFL{ZW{aRMPlx=>y2@>7A?u~g;T_~WTeShKQ zbL~@~@R?xX1=0oNmC}4=^!^Quuiof>Y0apRhM-j_rGvw{45nSfCcLkjWfG{N)g*Hh zOWP6kz9FXWRgi`V#qvh&mF09BPyNjETgiRCmZxS33YAZGUEQ#w_P7>&2I-7V++6j3 z{dGUwWp_6|iHecl@quwvnpFPsu@bF!8>=iRAZ&!EpkSXf^TaazFo&XFMedWm6>FFC z@x9-uKm8o=z^EqreDpJK^ls9bLI1=e@p-JieVvHK(7CPx@@rUn25CWU)5lf{OI`|sRk`8uPBJ)*>RX-lA! znNu};o8F^~8htwnt^9za6T^U7KZdMaR<>r96HXRR1dh|i

R|@g+Sa<==>zLVcr} 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 ZSgZ!~3=2jv +{ + constructor(protected readonly context: PluginInitializerContext) {} + + public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { + const start = createStartServicesGetter(core.getStartServices); + plugins.embeddable.registerEmbeddableFactory( + IMAGE_EMBEDDABLE_TYPE, + new ImageEmbeddableFactoryDefinition({ + start: () => ({ + application: start().core.application, + overlays: start().core.overlays, + files: start().plugins.files.filesClientFactory.asUnscoped(), + externalUrl: start().core.http.externalUrl, + theme: start().core.theme, + }), + }) + ); + return {}; + } + + public start(core: CoreStart, plugins: StartDependencies): StartContract { + return {}; + } + + public stop() {} +} diff --git a/src/plugins/image_embeddable/public/types.ts b/src/plugins/image_embeddable/public/types.ts new file mode 100644 index 0000000000000..4d04be3e8a869 --- /dev/null +++ b/src/plugins/image_embeddable/public/types.ts @@ -0,0 +1,31 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface ImageConfig { + src: ImageFileSrc | ImageUrlSrc; + altText?: string; + sizing: { + objectFit: `fill` | `contain` | `cover` | `none`; + }; + backgroundColor?: string; +} + +export interface ImageFileSrc { + type: 'file'; + fileId: string; + fileImageMeta: { + blurHash?: string; + width: number; + height: number; + }; +} + +export interface ImageUrlSrc { + type: 'url'; + url: string; +} diff --git a/src/plugins/image_embeddable/public/utils/validate_image_config.ts b/src/plugins/image_embeddable/public/utils/validate_image_config.ts new file mode 100644 index 0000000000000..6c44c714109fa --- /dev/null +++ b/src/plugins/image_embeddable/public/utils/validate_image_config.ts @@ -0,0 +1,27 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { RecursivePartial } from '@elastic/eui'; +import { ImageConfig } from '../types'; +import { ValidateUrlFn } from './validate_url'; + +export type DraftImageConfig = RecursivePartial; +export function validateImageConfig( + draftConfig: DraftImageConfig, + { validateUrl }: { validateUrl: ValidateUrlFn } +): draftConfig is ImageConfig { + if (!draftConfig.src) return false; + if (draftConfig.src.type === 'file') { + if (!draftConfig.src.fileId) return false; + } else if (draftConfig.src.type === 'url') { + if (!draftConfig.src.url) return false; + if (!validateUrl(draftConfig.src.url).isValid) return false; + } + + return true; +} diff --git a/src/plugins/image_embeddable/public/utils/validate_url.ts b/src/plugins/image_embeddable/public/utils/validate_url.ts new file mode 100644 index 0000000000000..14ef38f3a2982 --- /dev/null +++ b/src/plugins/image_embeddable/public/utils/validate_url.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { IExternalUrl } from '@kbn/core-http-browser'; + +const SAFE_URL_PATTERN = /^(?:(?:https?):|[^&:/?#]*(?:[/?#]|$))/gi; +const generalFormatError = i18n.translate( + 'imageEmbeddable.imageEditor.urlFormatGeneralErrorMessage', + { + defaultMessage: 'Invalid format. Example: {exampleUrl}', + values: { + exampleUrl: 'https://elastic.co/my-image.png', + }, + } +); + +const externalUrlError = i18n.translate( + 'imageEmbeddable.imageEditor.urlFormatExternalErrorMessage', + { + defaultMessage: + 'This URL is not allowed by your administrator. Refer to "externalUrl.policy" configuration.', + } +); + +export type ValidateUrlFn = ReturnType; + +export function createValidateUrl( + externalUrl: IExternalUrl +): (url: string) => { isValid: boolean; error?: string } { + return (url: string) => { + if (!url) + return { + isValid: false, + error: generalFormatError, + }; + + try { + new URL(url); + if (!url.match(SAFE_URL_PATTERN)) throw new Error(); + + const isExternalUrlValid = !!externalUrl.validateUrl(url); + if (!isExternalUrlValid) { + return { + isValid: false, + error: externalUrlError, + }; + } + + return { isValid: true }; + } catch (e) { + return { + isValid: false, + error: generalFormatError, + }; + } + }; +} diff --git a/src/plugins/image_embeddable/tsconfig.json b/src/plugins/image_embeddable/tsconfig.json new file mode 100644 index 0000000000000..c3cec07779c30 --- /dev/null +++ b/src/plugins/image_embeddable/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*", "common/**/*", "server/**/*"], + "kbn_references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../embeddable/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../files/tsconfig.json" } + ] +} diff --git a/src/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx b/src/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx index 6d84e52f5344c..8638a877b9c12 100644 --- a/src/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx +++ b/src/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx @@ -84,8 +84,16 @@ export class CustomTimeRangeAction implements Action { const isMarkdown = isVisualizeEmbeddable(embeddable) && (embeddable as VisualizeEmbeddable).getOutput().visTypeName === 'markdown'; + + const isImage = embeddable.type === 'image'; + return Boolean( - embeddable && embeddable.parent && hasTimeRange(embeddable) && !isInputControl && !isMarkdown + embeddable && + embeddable.parent && + hasTimeRange(embeddable) && + !isInputControl && + !isMarkdown && + !isImage ); } diff --git a/test/functional/apps/dashboard_elements/image_embeddable/elastic_logo.png b/test/functional/apps/dashboard_elements/image_embeddable/elastic_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..085012eac378818bf04ffde92a9950243942003d GIT binary patch literal 34043 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 literal 0 HcmV?d00001 diff --git a/test/functional/apps/dashboard_elements/image_embeddable/image_embeddable.ts b/test/functional/apps/dashboard_elements/image_embeddable/image_embeddable.ts new file mode 100644 index 0000000000000..2f9aa81fe1eb3 --- /dev/null +++ b/test/functional/apps/dashboard_elements/image_embeddable/image_embeddable.ts @@ -0,0 +1,48 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'dashboard']); + const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + + describe('image embeddable', function () { + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/dashboard/current/kibana' + ); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.switchToEditMode(); + }); + + it('should create an image embeddable', async () => { + // create an image embeddable + await testSubjects.click(`dashboardQuickButtonimage`); + await testSubjects.exists(`createImageEmbeddableFlyout`); + await PageObjects.common.setFileInputPath(require.resolve('./elastic_logo.png')); + await testSubjects.clickWhenNotDisabled(`imageEmbeddableEditorSave`); + + // check that it is added on the dashboard + expect(await PageObjects.dashboard.getSharedItemsCount()).to.be('1'); + await PageObjects.dashboard.waitForRenderComplete(); + const panel = (await PageObjects.dashboard.getDashboardPanels())[0]; + const img = await panel.findByCssSelector('img.euiImage'); + const imgSrc = await img.getAttribute('src'); + expect(imgSrc).to.contain(`files/defaultImage`); + }); + }); +} diff --git a/test/functional/apps/dashboard_elements/image_embeddable/index.ts b/test/functional/apps/dashboard_elements/image_embeddable/index.ts new file mode 100644 index 0000000000000..c32d84c4bb5b3 --- /dev/null +++ b/test/functional/apps/dashboard_elements/image_embeddable/index.ts @@ -0,0 +1,15 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('image embeddable', function () { + loadTestFile(require.resolve('./image_embeddable')); + }); +} diff --git a/test/functional/apps/dashboard_elements/index.ts b/test/functional/apps/dashboard_elements/index.ts index 6866fcd0b7188..f6332057b89b7 100644 --- a/test/functional/apps/dashboard_elements/index.ts +++ b/test/functional/apps/dashboard_elements/index.ts @@ -34,6 +34,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./input_control_vis')); loadTestFile(require.resolve('./controls')); loadTestFile(require.resolve('./_markdown_vis')); + loadTestFile(require.resolve('./image_embeddable')); }); }); } diff --git a/tsconfig.base.json b/tsconfig.base.json index 713f0784f1217..c4770118c8385 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -868,6 +868,8 @@ "@kbn/guided-onboarding-plugin/*": ["src/plugins/guided_onboarding/*"], "@kbn/home-plugin": ["src/plugins/home"], "@kbn/home-plugin/*": ["src/plugins/home/*"], + "@kbn/image-embeddable-plugin": ["src/plugins/image_embeddable"], + "@kbn/image-embeddable-plugin/*": ["src/plugins/image_embeddable/*"], "@kbn/input-control-vis-plugin": ["src/plugins/input_control_vis"], "@kbn/input-control-vis-plugin/*": ["src/plugins/input_control_vis/*"], "@kbn/inspector-plugin": ["src/plugins/inspector"], From 10c4989a19c0db8ef9991ebed927f8521bb6e806 Mon Sep 17 00:00:00 2001 From: Kevin Lacabane Date: Mon, 19 Dec 2022 15:04:55 +0100 Subject: [PATCH 08/55] [Stack Monitoring] Add support for beats datastream patterns (#146184) ## Summary Closes https://github.com/elastic/kibana/issues/146686 Update apm and beats queries to read from the Beat package data streams created in https://github.com/elastic/integrations/pull/4708. Also updates health API to fetch from the data streams. API tests follow up in https://github.com/elastic/kibana/pull/147755 ### Testing That's a fairly heavy workflow. I'm investigating ways to make that easier - build `beat` package with `elastic-package build`. The package is not published yet so you'll need to pull https://github.com/elastic/integrations/pull/4708 locally - start a stack `elastic-package stack up -v -d --version 8.7.0-SNAPSHOT`. Make sure you do this within `integrations` repo so it picks up the previously built package - start a beat service with `elastic-package service up -v --variant metricbeat_8.7.0` - install both elasticsearch and beat packages from the kibana started by the stack command (`https://localhost:5601`). Nothing shows up in SM if we don't have elasticsearch data. - both packages are prerelease versions and we need to explicitly tell Integrations plugin to show them up[1] - elasticsearch hosts is `https://elasticsearch:9200` and beat _should be_ `http://elastic-package-service_beat_1:5066` but it may differ depending on your docker version - start a Kibana with this branch connected to the elasticsearch instance from the `stack up` command. [see howto](https://github.com/elastic/observability-dev/blob/main/docs/infra-obs-ui/stack-monitoring_integration-packages.md#connecting-a-local-kibana) - navigate to Stack Monitoring and verify the metricbeat is properly monitored - start an apm server with `elastic-package service up -v --variant apm_8.7.0` - navigate to Stack Monitoring and verify apm-server is properly monitored [1] Screenshot 2022-12-01 at 00 20 20 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/apm/create_apm_query.ts | 3 ++- .../server/lib/apm/get_apms_for_clusters.ts | 5 +++-- .../server/lib/beats/create_beats_query.ts | 3 ++- .../lib/beats/get_beats_for_clusters.ts | 5 +++-- .../server/lib/cluster/get_index_patterns.ts | 1 + .../has_standalone_clusters.ts | 12 ++++++---- .../build_monitored_clusters.ts | 2 +- .../monitored_clusters_query.ts | 22 ++++++++++++++----- .../server/routes/api/v1/apm/instance.ts | 10 ++++++--- .../server/routes/api/v1/apm/instances.ts | 10 ++++++--- .../server/routes/api/v1/apm/overview.ts | 4 +++- .../server/routes/api/v1/beats/beat_detail.ts | 9 +++++--- .../server/routes/api/v1/beats/beats.ts | 9 +++++--- .../server/routes/api/v1/beats/overview.ts | 9 +++++--- 14 files changed, 71 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/monitoring/server/lib/apm/create_apm_query.ts b/x-pack/plugins/monitoring/server/lib/apm/create_apm_query.ts index 1dc0f7a31e9a7..85958eccfe2d4 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/create_apm_query.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/create_apm_query.ts @@ -7,6 +7,7 @@ import { ApmMetric, ApmMetricFields } from '../metrics'; import { createQuery } from '../create_query'; +import { getBeatDataset } from '../cluster/get_index_patterns'; /** * {@code createQuery} for all APM instances. @@ -27,7 +28,7 @@ export function createApmQuery(options: { metric: ApmMetric.getMetricFields(), type: 'beats_stats', metricset: 'stats', - dsDataset: 'beats.stats', + dsDataset: getBeatDataset('stats'), ...(options ?? {}), }; diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.ts b/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.ts index 2bf294b93fa1f..072f296fc52a3 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apms_for_clusters.ts @@ -11,7 +11,7 @@ import { ApmMetric } from '../metrics'; import { apmAggResponseHandler, apmUuidsAgg, apmAggFilterPath } from './_apm_stats'; import { getTimeOfLastEvent } from './_get_time_of_last_event'; import { ElasticsearchResponse } from '../../../common/types/es'; -import { getLegacyIndexPattern } from '../cluster/get_index_patterns'; +import { getIndexPatterns } from '../cluster/get_index_patterns'; import { Globals } from '../../static_globals'; export function handleResponse(clusterUuid: string, response: ElasticsearchResponse) { @@ -40,8 +40,9 @@ export function getApmsForClusters(req: LegacyRequest, clusters: Cluster[], ccs? const maxBucketSize = config.ui.max_bucket_size; const cgroup = config.ui.container.apm.enabled; - const indexPatterns = getLegacyIndexPattern({ + const indexPatterns = getIndexPatterns({ moduleType: 'beats', + dataset: 'stats', ccs: ccs || req.payload.ccs, config: Globals.app.config, }); diff --git a/x-pack/plugins/monitoring/server/lib/beats/create_beats_query.ts b/x-pack/plugins/monitoring/server/lib/beats/create_beats_query.ts index 6f56a95c8cbb9..9a59620a154ef 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/create_beats_query.ts +++ b/x-pack/plugins/monitoring/server/lib/beats/create_beats_query.ts @@ -7,6 +7,7 @@ import { BeatsMetric, BeatsMetricFields } from '../metrics'; import { createQuery } from '../create_query'; +import { getBeatDataset } from '../cluster/get_index_patterns'; /** * {@code createQuery} for all Beats instances. @@ -31,7 +32,7 @@ export function createBeatsQuery(options: { metric: BeatsMetric.getMetricFields(), type: 'beats_stats', metricset: 'stats', - dsDataset: 'beats.stats', + dsDataset: getBeatDataset('stats'), ...(options ?? {}), }; diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.ts b/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.ts index 6f42695fb7e99..f1914d0bc9f2b 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beats_for_clusters.ts @@ -10,7 +10,7 @@ import { createBeatsQuery } from './create_beats_query'; import { beatsAggFilterPath, beatsUuidsAgg, beatsAggResponseHandler } from './_beats_stats'; import type { ElasticsearchResponse } from '../../../common/types/es'; import { LegacyRequest, Cluster } from '../../types'; -import { getLegacyIndexPattern } from '../cluster/get_index_patterns'; +import { getIndexPatterns } from '../cluster/get_index_patterns'; import { Globals } from '../../static_globals'; export function handleResponse(clusterUuid: string, response: ElasticsearchResponse) { @@ -37,8 +37,9 @@ export function getBeatsForClusters(req: LegacyRequest, clusters: Cluster[], ccs const end = req.payload.timeRange.max; const config = req.server.config; const maxBucketSize = config.ui.max_bucket_size; - const indexPatterns = getLegacyIndexPattern({ + const indexPatterns = getIndexPatterns({ moduleType: 'beats', + dataset: 'stats', ccs, config: Globals.app.config, }); diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts index 03313f21fbfa2..8d31cea037864 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts @@ -126,6 +126,7 @@ const getDataset = (moduleType: INDEX_PATTERN_TYPES) => (dataset: string) => export const getElasticsearchDataset = getDataset('elasticsearch'); export const getKibanaDataset = getDataset('kibana'); export const getLogstashDataset = getDataset('logstash'); +export const getBeatDataset = getDataset('beats'); function buildDatasetPattern( moduleType?: INDEX_PATTERN_TYPES, diff --git a/x-pack/plugins/monitoring/server/lib/standalone_clusters/has_standalone_clusters.ts b/x-pack/plugins/monitoring/server/lib/standalone_clusters/has_standalone_clusters.ts index ca4e6cc843735..042340eb0669c 100644 --- a/x-pack/plugins/monitoring/server/lib/standalone_clusters/has_standalone_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/standalone_clusters/has_standalone_clusters.ts @@ -11,9 +11,9 @@ import { LegacyRequest } from '../../types'; import { standaloneClusterFilter } from '.'; import { Globals } from '../../static_globals'; import { - getLegacyIndexPattern, getIndexPatterns, getLogstashDataset, + getBeatDataset, } from '../cluster/get_index_patterns'; export async function hasStandaloneClusters(req: LegacyRequest, ccs: string) { @@ -22,8 +22,7 @@ export async function hasStandaloneClusters(req: LegacyRequest, ccs: string) { moduleType: 'logstash', ccs, }); - // use legacy because no integration exists for beats - const beatsIndexPatterns = getLegacyIndexPattern({ + const beatsIndexPatterns = getIndexPatterns({ moduleType: 'beats', config: Globals.app.config, ccs, @@ -51,7 +50,12 @@ export async function hasStandaloneClusters(req: LegacyRequest, ccs: string) { }, { terms: { - 'data_stream.dataset': [getLogstashDataset('node'), getLogstashDataset('node_stats')], + 'data_stream.dataset': [ + getLogstashDataset('node'), + getLogstashDataset('node_stats'), + getBeatDataset('state'), + getBeatDataset('stats'), + ], }, }, ], diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/build_monitored_clusters.ts b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/build_monitored_clusters.ts index 4b7a9f1c49856..2a621deff1854 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/build_monitored_clusters.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/build_monitored_clusters.ts @@ -43,7 +43,7 @@ const metricbeatMonitoring7Pattern = /(.*:)?\.monitoring-(es|kibana|beats|logstash|ent-search)-7.*-mb.*/; const metricbeatMonitoring8Pattern = /(.*:)?\.ds-\.monitoring-(es|kibana|beats|logstash|ent-search)-8-mb.*/; -const packagePattern = /(.*:)?\.ds-metrics-(elasticsearch|kibana|logstash)\..*/; +const packagePattern = /(.*:)?\.ds-metrics-(elasticsearch|kibana|beats|logstash)\..*/; const getCollectionMode = (index: string): CollectionMode => { if (internalMonitoringPattern.test(index)) return CollectionMode.Internal; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/monitored_clusters_query.ts b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/monitored_clusters_query.ts index 056039c19a7dc..5c889df19a1a5 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/monitored_clusters_query.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/monitored_clusters_query.ts @@ -6,15 +6,15 @@ */ import { TimeRange } from '../../../../../../common/http_api/shared'; +import { + getBeatDataset, + getElasticsearchDataset, + getKibanaDataset, + getLogstashDataset, +} from '../../../../../lib/cluster/get_index_patterns'; const MAX_BUCKET_SIZE = 100; -const getDataset = (product: string) => (metricset: string) => - `${product}.stack_monitoring.${metricset}`; -const getElasticsearchDataset = getDataset('elasticsearch'); -const getKibanaDataset = getDataset('kibana'); -const getLogstashDataset = getDataset('logstash'); - interface QueryOptions { timeRange?: TimeRange; timeout: number; // in seconds @@ -509,6 +509,11 @@ const beatsAggregations = { 'metricset.name': 'stats', }, }, + { + term: { + 'data_stream.dataset': getBeatDataset('stats'), + }, + }, ], }, }, @@ -543,6 +548,11 @@ const beatsAggregations = { 'metricset.name': 'state', }, }, + { + term: { + 'data_stream.dataset': getBeatDataset('state'), + }, + }, ], }, }, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.ts b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.ts index d6874748c1d6b..92266c20596dc 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { prefixIndexPatternWithCcs } from '../../../../../common/ccs_utils'; -import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; import { postApmInstanceRequestParamsRT, postApmInstanceRequestPayloadRT, @@ -16,6 +14,7 @@ import { getApmInfo } from '../../../../lib/apm'; import { createValidationFunction } from '../../../../lib/create_route_validation_function'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors'; +import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; import { MonitoringCore } from '../../../../types'; import { metricSet } from './metric_set_instance'; @@ -35,7 +34,12 @@ export function apmInstanceRoute(server: MonitoringCore) { const config = server.config; const clusterUuid = req.params.clusterUuid; const ccs = req.payload.ccs; - const apmIndexPattern = prefixIndexPatternWithCcs(config, INDEX_PATTERN_BEATS, ccs); + const apmIndexPattern = getIndexPatterns({ + ccs, + config, + moduleType: 'beats', + dataset: 'stats', + }); const showCgroupMetrics = config.ui.container.apm.enabled; if (showCgroupMetrics) { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.ts b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.ts index 75986e45f01d5..3dbe30c459ba6 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instances.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { prefixIndexPatternWithCcs } from '../../../../../common/ccs_utils'; -import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; import { postApmInstancesRequestParamsRT, postApmInstancesRequestPayloadRT, @@ -15,6 +13,7 @@ import { import { getApms, getStats } from '../../../../lib/apm'; import { createValidationFunction } from '../../../../lib/create_route_validation_function'; import { handleError } from '../../../../lib/errors'; +import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; import { MonitoringCore } from '../../../../types'; export function apmInstancesRoute(server: MonitoringCore) { @@ -32,7 +31,12 @@ export function apmInstancesRoute(server: MonitoringCore) { const config = server.config; const ccs = req.payload.ccs; const clusterUuid = req.params.clusterUuid; - const apmIndexPattern = prefixIndexPatternWithCcs(config, INDEX_PATTERN_BEATS, ccs); + const apmIndexPattern = getIndexPatterns({ + ccs, + config, + moduleType: 'beats', + dataset: 'stats', + }); try { const [stats, apms] = await Promise.all([ diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.ts b/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.ts index 3cc882c7d75ee..468f267d517bd 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/overview.ts @@ -43,7 +43,9 @@ export function apmOverviewRoute(server: MonitoringCore) { try { const [stats, metrics] = await Promise.all([ getApmClusterStatus(req, { clusterUuid }), - getMetrics(req, 'beats', metricSet), + getMetrics(req, 'beats', metricSet, [ + { term: { 'beats_stats.beat.type': 'apm-server' } }, + ]), ]); return postApmOverviewResponsePayloadRT.encode({ diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.ts b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.ts index 26477b3611360..1a6e1da429f93 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beat_detail.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { prefixIndexPatternWithCcs } from '../../../../../common/ccs_utils'; -import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; import { postBeatDetailRequestParamsRT, postBeatDetailRequestPayloadRT, @@ -16,6 +14,7 @@ import { getBeatSummary } from '../../../../lib/beats'; import { createValidationFunction } from '../../../../lib/create_route_validation_function'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors'; +import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; import { MonitoringCore } from '../../../../types'; import { metricSet } from './metric_set_detail'; @@ -35,7 +34,11 @@ export function beatsDetailRoute(server: MonitoringCore) { const beatUuid = req.params.beatUuid; const config = server.config; const ccs = req.payload.ccs; - const beatsIndexPattern = prefixIndexPatternWithCcs(config, INDEX_PATTERN_BEATS, ccs); + const beatsIndexPattern = getIndexPatterns({ + ccs, + config, + moduleType: 'beats', + }); const summaryOptions = { clusterUuid, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.ts b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.ts index 385950d23f836..f78a4902734fa 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/beats.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { prefixIndexPatternWithCcs } from '../../../../../common/ccs_utils'; -import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; import { postBeatsListingRequestParamsRT, postBeatsListingRequestPayloadRT, @@ -15,6 +13,7 @@ import { import { getBeats, getStats } from '../../../../lib/beats'; import { createValidationFunction } from '../../../../lib/create_route_validation_function'; import { handleError } from '../../../../lib/errors'; +import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; import { MonitoringCore } from '../../../../types'; export function beatsListingRoute(server: MonitoringCore) { @@ -32,7 +31,11 @@ export function beatsListingRoute(server: MonitoringCore) { const config = server.config; const ccs = req.payload.ccs; const clusterUuid = req.params.clusterUuid; - const beatsIndexPattern = prefixIndexPatternWithCcs(config, INDEX_PATTERN_BEATS, ccs); + const beatsIndexPattern = getIndexPatterns({ + ccs, + config, + moduleType: 'beats', + }); try { const [stats, listing] = await Promise.all([ diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.ts b/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.ts index f36d6cd0b1aeb..3fac0fea06db5 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/beats/overview.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { prefixIndexPatternWithCcs } from '../../../../../common/ccs_utils'; -import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; import { postBeatsOverviewRequestParamsRT, postBeatsOverviewRequestPayloadRT, @@ -16,6 +14,7 @@ import { getLatestStats, getStats } from '../../../../lib/beats'; import { createValidationFunction } from '../../../../lib/create_route_validation_function'; import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors'; +import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; import { MonitoringCore } from '../../../../types'; import { metricSet } from './metric_set_overview'; @@ -34,7 +33,11 @@ export function beatsOverviewRoute(server: MonitoringCore) { const config = server.config; const ccs = req.payload.ccs; const clusterUuid = req.params.clusterUuid; - const beatsIndexPattern = prefixIndexPatternWithCcs(config, INDEX_PATTERN_BEATS, ccs); + const beatsIndexPattern = getIndexPatterns({ + ccs, + config, + moduleType: 'beats', + }); try { const [latest, stats, metrics] = await Promise.all([ From 3061ac30911868bff56909c865885154a9d92c3d Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 19 Dec 2022 15:15:56 +0100 Subject: [PATCH 09/55] [RAM] Use bulk delete everywhere in UI (#147570) Solves: https://github.com/elastic/kibana/issues/147437 ## Summary - Main purpose of this PR is to use _bulk_delete API everywhere where now i used just single delete - Remove single delete rule api in UI - Fix bulkDeleteResponse type ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../components/delete_modal_confirmation.tsx | 22 +++--- .../public/pages/rule_details/index.tsx | 4 +- .../application/lib/rule_api/bulk_delete.ts | 13 +--- .../application/lib/rule_api/delete.test.ts | 32 --------- .../public/application/lib/rule_api/delete.ts | 30 -------- .../public/application/lib/rule_api/index.ts | 1 - .../with_bulk_rule_api_operations.test.tsx | 32 +++------ .../with_bulk_rule_api_operations.tsx | 22 +----- .../components/rule_details.test.tsx | 15 ++-- .../rule_details/components/rule_details.tsx | 69 ++++++++++++------- .../rules_list/components/rules_list.tsx | 24 +++++-- .../rules_list_bulk_delete.test.tsx | 2 +- .../triggers_actions_ui/public/index.ts | 3 +- .../triggers_actions_ui/public/types.ts | 5 -- 14 files changed, 102 insertions(+), 172 deletions(-) delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.test.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.ts diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/delete_modal_confirmation.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/delete_modal_confirmation.tsx index 61eec8b8c3ea8..0738362402566 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/components/delete_modal_confirmation.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/components/delete_modal_confirmation.tsx @@ -7,7 +7,10 @@ import { EuiConfirmModal } from '@elastic/eui'; import React, { useEffect, useState } from 'react'; -import { HttpSetup } from '@kbn/core/public'; +import type { + BulkOperationAttributes, + BulkOperationResponse, +} from '@kbn/triggers-actions-ui-plugin/public'; import { useKibana } from '../../../utils/kibana_react'; import { confirmModalText, @@ -28,14 +31,8 @@ export function DeleteModalConfirmation({ setIsLoadingState, }: { idsToDelete: string[]; - apiDeleteCall: ({ - ids, - http, - }: { - ids: string[]; - http: HttpSetup; - }) => Promise<{ successes: string[]; errors: string[] }>; - onDeleted: (deleted: string[]) => void; + apiDeleteCall: ({ ids, http }: BulkOperationAttributes) => Promise; + onDeleted: () => void; onCancel: () => void; onErrors: () => void; singleTitle: string; @@ -69,11 +66,12 @@ export function DeleteModalConfirmation({ onConfirm={async () => { setDeleteModalVisibility(false); setIsLoadingState(true); - const { successes, errors } = await apiDeleteCall({ ids: idsToDelete, http }); + const { total, errors } = await apiDeleteCall({ ids: idsToDelete, http }); setIsLoadingState(false); - const numSuccesses = successes.length; const numErrors = errors.length; + const numSuccesses = total - numErrors; + if (numSuccesses > 0) { toasts.addSuccess(deleteSuccessText(numSuccesses, singleTitle, multipleTitle)); } @@ -82,7 +80,7 @@ export function DeleteModalConfirmation({ toasts.addDanger(deleteErrorText(numErrors, singleTitle, multipleTitle)); await onErrors(); } - await onDeleted(successes); + await onDeleted(); }} cancelButtonText={cancelButtonText} confirmButtonText={confirmButtonText(numIdsToDelete, singleTitle, multipleTitle)} diff --git a/x-pack/plugins/observability/public/pages/rule_details/index.tsx b/x-pack/plugins/observability/public/pages/rule_details/index.tsx index 2225e5f9170dd..bb856d94862b4 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/index.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/index.tsx @@ -26,7 +26,7 @@ import { } from '@elastic/eui'; import { - deleteRules, + bulkDeleteRules, useLoadRuleTypes, RuleType, getNotifyWhenOptions, @@ -424,7 +424,7 @@ export function RuleDetailsPage() { navigateToUrl(http.basePath.prepend(paths.observability.rules)); }} onCancel={() => setRuleToDelete([])} - apiDeleteCall={deleteRules} + apiDeleteCall={bulkDeleteRules} idsToDelete={ruleToDelete} singleTitle={rule.name} multipleTitle={rule.name} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_delete.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_delete.ts index 977b81b6c9673..efc2468b8cd03 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_delete.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_delete.ts @@ -4,26 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { HttpSetup } from '@kbn/core/public'; -import { KueryNode } from '@kbn/es-query'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; -import { BulkDeleteResponse } from '../../../types'; +import { BulkOperationResponse, BulkOperationAttributes } from '../../../types'; export const bulkDeleteRules = async ({ filter, ids, http, -}: { - filter?: KueryNode | null; - ids?: string[]; - http: HttpSetup; -}): Promise => { +}: BulkOperationAttributes): Promise => { try { const body = JSON.stringify({ - ids: ids?.length ? ids : undefined, + ...(ids?.length ? { ids } : {}), ...(filter ? { filter: JSON.stringify(filter) } : {}), }); - return http.patch(`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_bulk_delete`, { body }); } catch (e) { throw new Error(`Unable to parse bulk delete params: ${e}`); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.test.ts deleted file mode 100644 index c75314d083e5c..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.test.ts +++ /dev/null @@ -1,32 +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 { httpServiceMock } from '@kbn/core/public/mocks'; -import { deleteRules } from './delete'; - -const http = httpServiceMock.createStartContract(); - -describe('deleteRules', () => { - test('should call delete API for each alert', async () => { - const ids = ['1', '2', '/']; - const result = await deleteRules({ http, ids }); - expect(result).toEqual({ errors: [], successes: [undefined, undefined, undefined] }); - expect(http.delete.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/alerting/rule/1", - ], - Array [ - "/api/alerting/rule/2", - ], - Array [ - "/api/alerting/rule/%2F", - ], - ] - `); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.ts deleted file mode 100644 index 6c0a37c538420..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.ts +++ /dev/null @@ -1,30 +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 { HttpSetup } from '@kbn/core/public'; -import { BASE_ALERTING_API_PATH } from '../../constants'; - -export async function deleteRules({ - ids, - http, -}: { - ids: string[]; - http: HttpSetup; -}): Promise<{ successes: string[]; errors: string[] }> { - const successes: string[] = []; - const errors: string[] = []; - await Promise.all( - ids.map((id) => http.delete(`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}`)) - ).then( - function (fulfilled) { - successes.push(...fulfilled); - }, - function (rejected) { - errors.push(...rejected); - } - ); - return { successes, errors }; -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts index b7b6338afe04d..f1d65768802ec 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts @@ -10,7 +10,6 @@ export type { LoadRuleAggregationsProps } from './aggregate_helpers'; export { loadRuleAggregations, loadRuleTags } from './aggregate'; export { createRule } from './create'; export { cloneRule } from './clone'; -export { deleteRules } from './delete'; export { loadRule } from './get_rule'; export { loadRuleSummary } from './rule_summary'; export { muteAlertInstance } from './mute_alert'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.test.tsx index dfd51857f392f..dbe65577844e0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.test.tsx @@ -30,10 +30,9 @@ describe('with_bulk_rule_api_operations', () => { expect(typeof props.unmuteRules).toEqual('function'); expect(typeof props.bulkEnableRules).toEqual('function'); expect(typeof props.bulkDisableRules).toEqual('function'); - expect(typeof props.deleteRules).toEqual('function'); + expect(typeof props.bulkDeleteRules).toEqual('function'); expect(typeof props.muteRule).toEqual('function'); expect(typeof props.unmuteRule).toEqual('function'); - expect(typeof props.deleteRule).toEqual('function'); expect(typeof props.loadRule).toEqual('function'); expect(typeof props.loadRuleTypes).toEqual('function'); expect(typeof props.resolveRule).toEqual('function'); @@ -106,21 +105,6 @@ describe('with_bulk_rule_api_operations', () => { expect(ruleApi.bulkDisableRules).toHaveBeenCalledWith({ ids: [rule.id], http }); }); - it('deleteRule calls the deleteRule api', () => { - const { http } = useKibanaMock().services; - const ComponentToExtend = ({ deleteRule, rule }: ComponentOpts & { rule: Rule }) => { - return ; - }; - - const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); - const rule = mockRule(); - const component = mount(); - component.find('button').simulate('click'); - - expect(ruleApi.deleteRules).toHaveBeenCalledTimes(1); - expect(ruleApi.deleteRules).toHaveBeenCalledWith({ ids: [rule.id], http }); - }); - // bulk rules it('muteRules calls the muteRules api', () => { const { http } = useKibanaMock().services; @@ -200,10 +184,14 @@ describe('with_bulk_rule_api_operations', () => { }); }); - it('deleteRules calls the deleteRules api', () => { + it('bulkDeleteRules calls the bulkDeleteRules api', () => { const { http } = useKibanaMock().services; - const ComponentToExtend = ({ deleteRules, rules }: ComponentOpts & { rules: Rule[] }) => { - return ; + const ComponentToExtend = ({ bulkDeleteRules, rules }: ComponentOpts & { rules: Rule[] }) => { + return ( + + ); }; const ExtendedComponent = withBulkRuleOperations(ComponentToExtend); @@ -211,8 +199,8 @@ describe('with_bulk_rule_api_operations', () => { const component = mount(); component.find('button').simulate('click'); - expect(ruleApi.deleteRules).toHaveBeenCalledTimes(1); - expect(ruleApi.deleteRules).toHaveBeenCalledWith({ ids: [rules[0].id, rules[1].id], http }); + expect(ruleApi.bulkDeleteRules).toHaveBeenCalledTimes(1); + expect(ruleApi.bulkDeleteRules).toHaveBeenCalledWith({ ids: [rules[0].id, rules[1].id], http }); }); it('loadRule calls the loadRule api', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx index e98536c277aec..4fe559e090550 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_rule_api_operations.tsx @@ -12,7 +12,6 @@ import { IExecutionErrorsResult, IExecutionKPIResult, } from '@kbn/alerting-plugin/common'; -import { KueryNode } from '@kbn/es-query'; import { Rule, RuleType, @@ -22,12 +21,10 @@ import { ResolvedRule, SnoozeSchedule, BulkEditResponse, - BulkDeleteResponse, BulkOperationResponse, BulkOperationAttributesWithoutHttp, } from '../../../../types'; import { - deleteRules, muteRules, unmuteRules, muteRule, @@ -66,18 +63,10 @@ import { useKibana } from '../../../../common/lib/kibana'; export interface ComponentOpts { muteRules: (rules: Rule[]) => Promise; unmuteRules: (rules: Rule[]) => Promise; - deleteRules: (rules: Rule[]) => Promise<{ - successes: string[]; - errors: string[]; - }>; muteRule: (rule: Rule) => Promise; unmuteRule: (rule: Rule) => Promise; muteAlertInstance: (rule: Rule, alertInstanceId: string) => Promise; unmuteAlertInstance: (rule: Rule, alertInstanceId: string) => Promise; - deleteRule: (rule: Rule) => Promise<{ - successes: string[]; - errors: string[]; - }>; loadRule: (id: Rule['id']) => Promise; loadRuleState: (id: Rule['id']) => Promise; loadRuleSummary: (id: Rule['id'], numberOfExecutions?: number) => Promise; @@ -102,10 +91,7 @@ export interface ComponentOpts { unsnoozeRule: (rule: Rule, scheduleIds?: string[]) => Promise; bulkUnsnoozeRules: (props: BulkUnsnoozeRulesProps) => Promise; cloneRule: (ruleId: string) => Promise; - bulkDeleteRules: (props: { - filter?: KueryNode | null; - ids?: string[]; - }) => Promise; + bulkDeleteRules: (props: BulkOperationAttributesWithoutHttp) => Promise; bulkEnableRules: (props: BulkOperationAttributesWithoutHttp) => Promise; bulkDisableRules: (props: BulkOperationAttributesWithoutHttp) => Promise; } @@ -129,9 +115,6 @@ export function withBulkRuleOperations( unmuteRules={async (items: Rule[]) => unmuteRules({ http, ids: items.filter(isRuleMuted).map((item) => item.id) }) } - deleteRules={async (items: Rule[]) => - deleteRules({ http, ids: items.map((item) => item.id) }) - } muteRule={async (rule: Rule) => { if (!isRuleMuted(rule)) { return await muteRule({ http, id: rule.id }); @@ -152,7 +135,6 @@ export function withBulkRuleOperations( return unmuteAlertInstance({ http, id: rule.id, instanceId }); } }} - deleteRule={async (rule: Rule) => deleteRules({ http, ids: [rule.id] })} loadRule={async (ruleId: Rule['id']) => loadRule({ http, ruleId })} loadRuleState={async (ruleId: Rule['id']) => loadRuleState({ http, ruleId })} loadRuleSummary={async (ruleId: Rule['id'], numberOfExecutions?: number) => @@ -212,7 +194,7 @@ export function withBulkRuleOperations( cloneRule={async (ruleId: string) => { return await cloneRule({ http, ruleId }); }} - bulkDeleteRules={async (bulkDeleteProps: { filter?: KueryNode | null; ids?: string[] }) => { + bulkDeleteRules={async (bulkDeleteProps: BulkOperationAttributesWithoutHttp) => { return await bulkDeleteRules({ http, ...bulkDeleteProps }); }} bulkEnableRules={async (bulkEnableProps: BulkOperationAttributesWithoutHttp) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx index e27e50785ac49..54830bd09fe56 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx @@ -48,7 +48,7 @@ jest.mock('../../../lib/rule_api', () => ({ bulkUpdateAPIKey: jest.fn(), deleteRules: jest.fn(), })); -const { bulkUpdateAPIKey, deleteRules } = jest.requireMock('../../../lib/rule_api'); +const { bulkUpdateAPIKey } = jest.requireMock('../../../lib/rule_api'); jest.mock('../../../lib/capabilities', () => ({ hasAllPrivilege: jest.fn(() => true), @@ -68,6 +68,7 @@ const mockRuleApis = { unsnoozeRule: jest.fn(), bulkEnableRules: jest.fn(), bulkDisableRules: jest.fn(), + bulkDeleteRules: jest.fn(), }; const authorizedConsumers = { @@ -691,7 +692,11 @@ describe('rule_details', () => { describe('delete rule button', () => { it('should delete the rule when clicked', async () => { - deleteRules.mockResolvedValueOnce({ successes: ['1'], errors: [] }); + mockRuleApis.bulkDeleteRules.mockResolvedValueOnce({ + rules: [{ id: 1 }], + errors: [], + total: 1, + }); const rule = mockRule(); const requestRefresh = jest.fn(); const wrapper = mountWithIntl( @@ -716,7 +721,7 @@ describe('rule_details', () => { updateButton.simulate('click'); - const confirm = wrapper.find('[data-test-subj="deleteIdsConfirmation"]').first(); + const confirm = wrapper.find('[data-test-subj="rulesDeleteConfirmation"]').first(); expect(confirm.exists()).toBeTruthy(); const confirmButton = wrapper.find('[data-test-subj="confirmModalConfirmButton"]').last(); @@ -724,8 +729,8 @@ describe('rule_details', () => { confirmButton.simulate('click'); - expect(deleteRules).toHaveBeenCalledTimes(1); - expect(deleteRules).toHaveBeenCalledWith(expect.objectContaining({ ids: [rule.id] })); + expect(mockRuleApis.bulkDeleteRules).toHaveBeenCalledTimes(1); + expect(mockRuleApis.bulkDeleteRules).toHaveBeenCalledWith({ ids: [rule.id] }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx index a1c56c91ebace..32a0e35cc2e11 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx @@ -27,8 +27,8 @@ import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { RuleExecutionStatusErrorReasons, parseDuration } from '@kbn/alerting-plugin/common'; import { getRuleDetailsRoute } from '@kbn/rule-data-utils'; import { UpdateApiKeyModalConfirmation } from '../../../components/update_api_key_modal_confirmation'; -import { bulkUpdateAPIKey, deleteRules } from '../../../lib/rule_api'; -import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation'; +import { bulkUpdateAPIKey } from '../../../lib/rule_api'; +import { RulesDeleteModalConfirmation } from '../../../components/rules_delete_modal_confirmation'; import { RuleActionsPopover } from './rule_actions_popover'; import { hasAllPrivilege, @@ -61,6 +61,13 @@ import { ruleReducer } from '../../rule_form/rule_reducer'; import { loadAllActions as loadConnectors } from '../../../lib/action_connector_api'; import { triggersActionsUiConfig } from '../../../../common/lib/config_api'; import { runRule } from '../../../lib/run_rule'; +import { + getConfirmDeletionButtonText, + getConfirmDeletionModalText, + SINGLE_RULE_TITLE, + MULTIPLE_RULE_TITLE, +} from '../../rules_list/translations'; +import { useBulkOperationToast } from '../../../hooks/use_bulk_operation_toast'; export type RuleDetailsProps = { rule: Rule; @@ -70,7 +77,7 @@ export type RuleDetailsProps = { refreshToken?: number; } & Pick< BulkOperationsComponentOpts, - 'bulkDisableRules' | 'bulkEnableRules' | 'snoozeRule' | 'unsnoozeRule' + 'bulkDisableRules' | 'bulkEnableRules' | 'bulkDeleteRules' | 'snoozeRule' | 'unsnoozeRule' >; const ruleDetailStyle = { @@ -83,6 +90,7 @@ export const RuleDetails: React.FunctionComponent = ({ actionTypes, bulkDisableRules, bulkEnableRules, + bulkDeleteRules, snoozeRule, unsnoozeRule, requestRefresh, @@ -266,29 +274,41 @@ export const RuleDetails: React.FunctionComponent = ({ ) : null; + const [isDeleteModalFlyoutVisible, setIsDeleteModalVisibility] = useState(false); + const { showToast } = useBulkOperationToast({}); + + const onDeleteConfirm = async () => { + setIsDeleteModalVisibility(false); + const { errors, total } = await bulkDeleteRules({ + ids: rulesToDelete, + }); + showToast({ action: 'DELETE', errors, total }); + setRulesToDelete([]); + goToRulesList(); + }; + const onDeleteCancel = () => { + setIsDeleteModalVisibility(false); + setRulesToDelete([]); + }; + return ( <> - { - setRulesToDelete([]); - goToRulesList(); - }} - onErrors={async () => { - // Refresh the rule from the server, it may have been deleted - await requestRefresh(); - setRulesToDelete([]); - }} - onCancel={() => { - setRulesToDelete([]); - }} - apiDeleteCall={deleteRules} - idsToDelete={rulesToDelete} - singleTitle={i18n.translate('xpack.triggersActionsUI.sections.rulesList.singleTitle', { - defaultMessage: 'rule', - })} - multipleTitle="" - setIsLoadingState={() => {}} - /> + {isDeleteModalFlyoutVisible && ( + + )} { setRulesToUpdateAPIKey([]); @@ -358,6 +378,7 @@ export const RuleDetails: React.FunctionComponent = ({ canSaveRule={canSaveRule} rule={rule} onDelete={(ruleId) => { + setIsDeleteModalVisibility(true); setRulesToDelete([ruleId]); }} onApiKeyUpdate={(ruleId) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index ebbe92431d092..76e8f8cbc813d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -1055,7 +1055,10 @@ export const RulesList = ({ const [isDeleteModalFlyoutVisible, setIsDeleteModalVisibility] = useState(false); useEffect(() => { - setIsDeleteModalVisibility(rulesToDelete.length > 0 || Boolean(rulesToDeleteFilter)); + setIsDeleteModalVisibility( + (!isAllSelected && rulesToDelete.length > 0) || + (isAllSelected && Boolean(rulesToDeleteFilter)) + ); }, [rulesToDelete, rulesToDeleteFilter]); const { showToast } = useBulkOperationToast({ onSearchPopulate }); @@ -1090,22 +1093,29 @@ export const RulesList = ({ setIsDeleteModalVisibility(false); clearRulesToDelete(); }; + const onDeleteConfirm = useCallback(async () => { setIsDeleteModalVisibility(false); setIsDeletingRules(true); - const { errors, total } = await bulkDeleteRules({ - filter: rulesToDeleteFilter, - ids: rulesToDelete, - http, - }); + const bulkDeleteRulesArguments = + isAllSelected && rulesToDeleteFilter + ? { + filter: rulesToDeleteFilter, + http, + } + : { + ids: rulesToDelete, + http, + }; + const { errors, total } = await bulkDeleteRules(bulkDeleteRulesArguments); setIsDeletingRules(false); showToast({ action: 'DELETE', errors, total }); await refreshRules(); clearRulesToDelete(); onClearSelection(); - }, [http, rulesToDelete, rulesToDeleteFilter, setIsDeletingRules, toasts]); + }, [http, rulesToDelete, rulesToDeleteFilter]); const numberRulesToDelete = rulesToDelete.length || numberOfSelectedItems; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_delete.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_delete.test.tsx index 5ca9c5a59c954..1b4b3cf7970ae 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_delete.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list_bulk_delete.test.tsx @@ -173,7 +173,7 @@ describe.skip('Rules list bulk delete', () => { expect(filter.arguments[1].arguments[0].arguments[1].value).toEqual('alert:2'); expect(bulkDeleteRules).toHaveBeenCalledWith( - expect.objectContaining({ + expect.not.objectContaining({ ids: [], }) ); diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index b9183fa50bb6e..23057e35fdef3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -134,8 +134,8 @@ export { loadExecutionLogAggregations } from './application/lib/rule_api/load_ex export { loadActionErrorLog } from './application/lib/rule_api/load_action_error_log'; export { loadRuleTypes } from './application/lib/rule_api/rule_types'; export { loadRuleSummary } from './application/lib/rule_api/rule_summary'; -export { deleteRules } from './application/lib/rule_api/delete'; export { muteRule } from './application/lib/rule_api/mute'; +export { bulkDeleteRules } from './application/lib/rule_api/bulk_delete'; export { unmuteRule } from './application/lib/rule_api/unmute'; export { snoozeRule } from './application/lib/rule_api/snooze'; export { unsnoozeRule } from './application/lib/rule_api/unsnooze'; @@ -147,6 +147,7 @@ export { loadActionTypes } from './application/lib/action_connector_api/connecto export { TIME_UNITS } from './application/constants'; export { getTimeUnitLabel } from './common/lib/get_time_unit_label'; export type { TriggersAndActionsUiServices } from './application/app'; +export type { BulkOperationAttributes, BulkOperationResponse } from './types'; export const getNotifyWhenOptions = async () => { const { NOTIFY_WHEN_OPTIONS } = await import('./application/sections/rule_form/rule_notify_when'); diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index cdbe4cd066476..99f1e848e5ccf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -171,11 +171,6 @@ export enum ActionConnectorMode { ActionForm = 'actionForm', } -export interface BulkDeleteResponse { - errors: BulkOperationError[]; - total: number; -} - export interface BulkOperationResponse { rules: Rule[]; errors: BulkOperationError[]; From 90dae7782ab9464de13c7dfe528fae1aadce201f Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 19 Dec 2022 16:19:14 +0200 Subject: [PATCH 10/55] Fixes the styling of saved objects save modal (#147593) ## Summary With the latest EUI update the header of the saved objects save modal has the wrong styling. This PR fixes this. Before: image After: image --- .../saved_object_save_modal.test.tsx.snap | 72 ++++++++++--------- .../save_modal/saved_object_save_modal.tsx | 12 ++-- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap index 0f2367910cefd..1c4864ab139e5 100644 --- a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap +++ b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap @@ -8,15 +8,17 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` > - + + /> + @@ -107,15 +109,17 @@ exports[`SavedObjectSaveModal should render matching snapshot when custom isVali > - + + /> + @@ -206,15 +210,17 @@ exports[`SavedObjectSaveModal should render matching snapshot when custom isVali > - + + /> + @@ -305,15 +311,17 @@ exports[`SavedObjectSaveModal should render matching snapshot when given options > - + + /> + diff --git a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx index 68dc426d234d4..d4fc8faa126c2 100644 --- a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx +++ b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx @@ -135,11 +135,13 @@ export class SavedObjectSaveModal extends React.Component > - +

+ +

From 32de227cd1350dceffeae8b1093f4a81ad7103e9 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Mon, 19 Dec 2022 15:24:26 +0100 Subject: [PATCH 11/55] [Enterprise Search] Add copy button for pipelines (#147712) This ensures the copy button shows up for curl requests on the connectors pipeline page. --- .../ingest_pipelines/default_pipeline_item.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/default_pipeline_item.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/default_pipeline_item.tsx index 3e6ad933c3e82..576c3e304a443 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/default_pipeline_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/default_pipeline_item.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiAccordion, @@ -32,6 +32,19 @@ export const DefaultPipelineItem: React.FC<{ pipelineName: string; pipelineState: IngestPipelineParams; }> = ({ index, indexName, ingestionMethod, openModal, pipelineName, pipelineState }) => { + /** + * If we don't open the accordion on load, the curl code never shows the copy button + * Because if the accordion is closed, the code block is not present in the DOM + * And EuiCodeBlock doesn't show the copy button if it's virtualized (ie not present in DOM) + * It doesn't re-evaluate whether it's present in DOM if the inner text doesn't change + * Opening and closing makes sure it's present in DOM on initial load, so the copy button shows + * We use setImmediate to then close it on the next javascript loop + */ + const [accordionOpen, setAccordionOpen] = useState(true); + useEffect(() => { + setImmediate(() => setAccordionOpen(false)); + }, []); + return ( @@ -65,6 +78,8 @@ export const DefaultPipelineItem: React.FC<{ { defaultMessage: 'Ingest a document using cURL' } )} id="ingestPipelinesCurlAccordion" + forceState={accordionOpen ? 'open' : 'closed'} + onClick={() => setAccordionOpen(!accordionOpen)} > Date: Mon, 19 Dec 2022 16:08:20 +0100 Subject: [PATCH 12/55] [RAM] Bring back to life disable API integration (#147464) Solves: https://github.com/elastic/kibana/issues/146173, https://github.com/elastic/kibana/issues/146120, https://github.com/elastic/kibana/issues/146119, https://github.com/elastic/kibana/issues/146115 ## Summary In this PR I bring back to life disable API integration tests. Bulk disable integration tests flakiness connected with Conflict error 409, which happens from time to time. We use retry function to solve this conflicts. And now we retry 2 times. It seems sometimes its not enough, so we decided to increase it to 3. I've run integration tests after change using `https://buildkite.com/elastic/kibana-flaky-test-suite-runner/` 100 times. All builds end up green. So I feel confident to merge it. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/wait_before_next_retry.ts | 2 +- .../rules_client/methods/bulk_delete.ts | 1 + .../rules_client/methods/bulk_disable.ts | 10 +- .../server/rules_client/methods/bulk_edit.ts | 1 + .../rules_client/methods/bulk_enable.ts | 10 +- .../rules_client/tests/bulk_delete.test.ts | 92 ++++++------------- .../rules_client/tests/bulk_disable.test.ts | 11 ++- .../rules_client/tests/bulk_enable.test.ts | 11 ++- .../group3/tests/alerting/bulk_disable.ts | 71 +++++--------- .../group3/tests/alerting/bulk_enable.ts | 58 ++++-------- 10 files changed, 100 insertions(+), 167 deletions(-) diff --git a/x-pack/plugins/alerting/server/rules_client/common/wait_before_next_retry.ts b/x-pack/plugins/alerting/server/rules_client/common/wait_before_next_retry.ts index f836de26c4188..a983aafd1cf42 100644 --- a/x-pack/plugins/alerting/server/rules_client/common/wait_before_next_retry.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/wait_before_next_retry.ts @@ -5,7 +5,7 @@ * 2.0. */ -export const RETRY_IF_CONFLICTS_ATTEMPTS = 2; +export const RETRY_IF_CONFLICTS_ATTEMPTS = 3; // milliseconds to wait before retrying when conflicts occur // note: we considered making this random, to help avoid a stampede, but diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_delete.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_delete.ts index 6e581ab94a6d5..abce18abe6802 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_delete.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_delete.ts @@ -142,6 +142,7 @@ const bulkDeleteWithOCC = async ( ); } } + await rulesFinder.close(); const result = await context.unsecuredSavedObjectsClient.bulkDelete(rulesToDelete); diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_disable.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_disable.ts index c5ecb4d7b5137..316c3c9685a9b 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_disable.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_disable.ts @@ -5,7 +5,6 @@ * 2.0. */ -import pMap from 'p-map'; import { KueryNode, nodeBuilder } from '@kbn/es-query'; import { SavedObjectsBulkUpdateObject } from '@kbn/core/server'; import { RawRule } from '../../types'; @@ -129,10 +128,12 @@ const bulkDisableRulesWithOCC = async ( context: RulesClientContext, { filter }: { filter: KueryNode | null } ) => { + const additionalFilter = nodeBuilder.is('alert.attributes.enabled', 'true'); + const rulesFinder = await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( { - filter, + filter: filter ? nodeBuilder.and([filter, additionalFilter]) : additionalFilter, type: 'alert', perPage: 100, ...(context.namespace ? { namespaces: [context.namespace] } : undefined), @@ -142,9 +143,10 @@ const bulkDisableRulesWithOCC = async ( const rulesToDisable: Array> = []; const errors: BulkOperationError[] = []; const ruleNameToRuleIdMapping: Record = {}; + const username = await context.getUserName(); for await (const response of rulesFinder.find()) { - await pMap(response.saved_objects, async (rule) => { + response.saved_objects.forEach((rule) => { try { if (rule.attributes.enabled === false) return; @@ -154,7 +156,6 @@ const bulkDisableRulesWithOCC = async ( ruleNameToRuleIdMapping[rule.id] = rule.attributes.name; } - const username = await context.getUserName(); const updatedAttributes = updateMeta(context, { ...rule.attributes, enabled: false, @@ -195,6 +196,7 @@ const bulkDisableRulesWithOCC = async ( } }); } + await rulesFinder.close(); const result = await context.unsecuredSavedObjectsClient.bulkCreate(rulesToDisable, { overwrite: true, diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts index 0ac7c8d24d0fe..cae2ebc2406bb 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts @@ -504,6 +504,7 @@ async function bulkEditOcc( { concurrency: API_KEY_GENERATE_CONCURRENCY } ); } + await rulesFinder.close(); let result; try { diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts index 394f7aad0dea7..0546906b04313 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts @@ -115,10 +115,12 @@ const bulkEnableRulesWithOCC = async ( context: RulesClientContext, { filter }: { filter: KueryNode | null } ) => { + const additionalFilter = nodeBuilder.is('alert.attributes.enabled', 'false'); + const rulesFinder = await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( { - filter, + filter: filter ? nodeBuilder.and([filter, additionalFilter]) : additionalFilter, type: 'alert', perPage: 100, ...(context.namespace ? { namespaces: [context.namespace] } : undefined), @@ -129,10 +131,12 @@ const bulkEnableRulesWithOCC = async ( const taskIdsToEnable: string[] = []; const errors: BulkOperationError[] = []; const ruleNameToRuleIdMapping: Record = {}; + const username = await context.getUserName(); for await (const response of rulesFinder.find()) { await pMap(response.saved_objects, async (rule) => { try { + if (rule.attributes.enabled === true) return; if (rule.attributes.actions.length) { try { await context.actionsAuthorization.ensureAuthorized('execute'); @@ -140,13 +144,10 @@ const bulkEnableRulesWithOCC = async ( throw Error(`Rule not authorized for bulk enable - ${error.message}`); } } - if (rule.attributes.enabled === true) return; if (rule.attributes.name) { ruleNameToRuleIdMapping[rule.id] = rule.attributes.name; } - const username = await context.getUserName(); - const updatedAttributes = updateMeta(context, { ...rule.attributes, ...(!rule.attributes.apiKey && @@ -212,6 +213,7 @@ const bulkEnableRulesWithOCC = async ( } }); } + await rulesFinder.close(); const result = await context.unsecuredSavedObjectsClient.bulkCreate(rulesToEnable, { overwrite: true, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/bulk_delete.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/bulk_delete.test.ts index 19c020f171f3b..ec7d4d5ef008c 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/bulk_delete.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/bulk_delete.test.ts @@ -55,6 +55,17 @@ const rulesClientParams: jest.Mocked = { minimumScheduleInterval: { value: '1m', enforce: false }, }; +const getBulkOperationStatusErrorResponse = (statusCode: number) => ({ + id: 'id2', + type: 'alert', + success: false, + error: { + error: '', + message: 'UPS', + statusCode, + }, +}); + beforeEach(() => { getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry); jest.clearAllMocks(); @@ -117,16 +128,7 @@ describe('bulkDelete', () => { unsecuredSavedObjectsClient.bulkDelete.mockResolvedValue({ statuses: [ { id: 'id1', type: 'alert', success: true }, - { - id: 'id2', - type: 'alert', - success: false, - error: { - error: '', - message: 'UPS', - statusCode: 500, - }, - }, + getBulkOperationStatusErrorResponse(500), ], }); @@ -158,45 +160,17 @@ describe('bulkDelete', () => { .mockResolvedValueOnce({ statuses: [ { id: 'id1', type: 'alert', success: true }, - { - id: 'id2', - type: 'alert', - success: false, - error: { - error: '', - message: 'UPS', - statusCode: 409, - }, - }, + getBulkOperationStatusErrorResponse(409), ], }) .mockResolvedValueOnce({ - statuses: [ - { - id: 'id2', - type: 'alert', - success: false, - error: { - error: '', - message: 'UPS', - statusCode: 409, - }, - }, - ], + statuses: [getBulkOperationStatusErrorResponse(409)], }) .mockResolvedValueOnce({ - statuses: [ - { - id: 'id2', - type: 'alert', - success: false, - error: { - error: '', - message: 'UPS', - statusCode: 409, - }, - }, - ], + statuses: [getBulkOperationStatusErrorResponse(409)], + }) + .mockResolvedValueOnce({ + statuses: [getBulkOperationStatusErrorResponse(409)], }); encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest @@ -213,6 +187,12 @@ describe('bulkDelete', () => { yield { saved_objects: [enabledRule2] }; }, }) + .mockResolvedValueOnce({ + close: jest.fn(), + find: function* asyncGenerator() { + yield { saved_objects: [enabledRule2] }; + }, + }) .mockResolvedValueOnce({ close: jest.fn(), find: function* asyncGenerator() { @@ -222,7 +202,7 @@ describe('bulkDelete', () => { const result = await rulesClient.bulkDeleteRules({ ids: ['id1', 'id2'] }); - expect(unsecuredSavedObjectsClient.bulkDelete).toHaveBeenCalledTimes(3); + expect(unsecuredSavedObjectsClient.bulkDelete).toHaveBeenCalledTimes(4); expect(taskManager.bulkRemoveIfExist).toHaveBeenCalledTimes(1); expect(taskManager.bulkRemoveIfExist).toHaveBeenCalledWith(['id1']); expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1); @@ -244,16 +224,7 @@ describe('bulkDelete', () => { .mockResolvedValueOnce({ statuses: [ { id: 'id1', type: 'alert', success: true }, - { - id: 'id2', - type: 'alert', - success: false, - error: { - error: '', - message: 'UPS', - statusCode: 409, - }, - }, + getBulkOperationStatusErrorResponse(409), ], }) .mockResolvedValueOnce({ @@ -355,16 +326,7 @@ describe('bulkDelete', () => { type: 'alert', success: true, }, - { - id: 'id2', - type: 'alert', - success: false, - error: { - error: '', - message: 'UPS', - statusCode: 500, - }, - }, + getBulkOperationStatusErrorResponse(500), ], })); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/bulk_disable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/bulk_disable.test.ts index 88699a50195e6..ddb1a5da58e5b 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/bulk_disable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/bulk_disable.test.ts @@ -194,6 +194,9 @@ describe('bulkDisableRules', () => { .mockResolvedValueOnce({ saved_objects: [savedObjectWith409Error], }) + .mockResolvedValueOnce({ + saved_objects: [savedObjectWith409Error], + }) .mockResolvedValueOnce({ saved_objects: [savedObjectWith409Error], }); @@ -212,6 +215,12 @@ describe('bulkDisableRules', () => { yield { saved_objects: [enabledRule2] }; }, }) + .mockResolvedValueOnce({ + close: jest.fn(), + find: function* asyncGenerator() { + yield { saved_objects: [enabledRule2] }; + }, + }) .mockResolvedValueOnce({ close: jest.fn(), find: function* asyncGenerator() { @@ -221,7 +230,7 @@ describe('bulkDisableRules', () => { const result = await rulesClient.bulkDisableRules({ ids: ['id1', 'id2'] }); - expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(3); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(4); expect(taskManager.bulkDisable).toHaveBeenCalledTimes(1); expect(taskManager.bulkDisable).toHaveBeenCalledWith(['id1']); expect(result).toStrictEqual({ diff --git a/x-pack/plugins/alerting/server/rules_client/tests/bulk_enable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/bulk_enable.test.ts index a48db94bd13e4..c4d3e1a253d95 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/bulk_enable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/bulk_enable.test.ts @@ -194,6 +194,9 @@ describe('bulkEnableRules', () => { .mockResolvedValueOnce({ saved_objects: [savedObjectWith409Error], }) + .mockResolvedValueOnce({ + saved_objects: [savedObjectWith409Error], + }) .mockResolvedValueOnce({ saved_objects: [savedObjectWith409Error], }); @@ -212,6 +215,12 @@ describe('bulkEnableRules', () => { yield { saved_objects: [disabledRule2] }; }, }) + .mockResolvedValueOnce({ + close: jest.fn(), + find: function* asyncGenerator() { + yield { saved_objects: [disabledRule2] }; + }, + }) .mockResolvedValueOnce({ close: jest.fn(), find: function* asyncGenerator() { @@ -221,7 +230,7 @@ describe('bulkEnableRules', () => { const result = await rulesClient.bulkEnableRules({ ids: ['id1', 'id2'] }); - expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(3); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(4); expect(result).toStrictEqual({ errors: [{ message: 'UPS', rule: { id: 'id2', name: 'fakeName' }, status: 409 }], rules: [returnedRule1], diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts index 74fd9d6a695ba..f584772a07eb4 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts @@ -49,11 +49,10 @@ export default ({ getService }: FtrProviderContext) => { const es = getService('es'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - // FLAKY: https://github.com/elastic/kibana/issues/146115 - describe.skip('bulkDisableRules', () => { + describe('bulkDisableRules', () => { const objectRemover = new ObjectRemover(supertest); - after(() => objectRemover.removeAll()); + afterEach(() => objectRemover.removeAll()); const getScheduledTask = async (id: string) => { return await es.get({ @@ -66,14 +65,13 @@ export default ({ getService }: FtrProviderContext) => { const { user, space } = scenario; describe(scenario.id, () => { - afterEach(() => objectRemover.removeAll()); - it('should handle bulk disable of one rule appropriately based on id', async () => { const { body: createdRule } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestRuleData()) .expect(200); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_disable`) @@ -90,7 +88,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'global_read at space1': expect(response.body).to.eql({ @@ -99,7 +96,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'space_1_all_alerts_none_actions at space1': case 'superuser at space1': @@ -124,6 +120,7 @@ export default ({ getService }: FtrProviderContext) => { }) ) .expect(200); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_disable`) @@ -140,7 +137,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'global_read at space1': expect(response.body).to.eql({ @@ -150,7 +146,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': @@ -160,7 +155,6 @@ export default ({ getService }: FtrProviderContext) => { message: 'No rules found for bulk disable', }); expect(response.statusCode).to.eql(400); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -193,6 +187,7 @@ export default ({ getService }: FtrProviderContext) => { }) ) .expect(200); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_disable`) @@ -209,7 +204,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': @@ -221,7 +215,6 @@ export default ({ getService }: FtrProviderContext) => { message: 'No rules found for bulk disable', }); expect(response.statusCode).to.eql(400); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'superuser at space1': expect(response.body).to.eql({ @@ -252,6 +245,7 @@ export default ({ getService }: FtrProviderContext) => { }) ) .expect(200); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_disable`) @@ -268,7 +262,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'global_read at space1': expect(response.body).to.eql({ @@ -277,7 +270,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'superuser at space1': case 'space_1_all at space1': @@ -310,6 +302,9 @@ export default ({ getService }: FtrProviderContext) => { .expect(200) ) ); + rules.map((rule) => { + objectRemover.add(space.id, rule.body.id, 'rule', 'alerting'); + }); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_disable`) @@ -326,11 +321,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - await Promise.all( - rules.map((rule) => { - objectRemover.add(space.id, rule.body.id, 'rule', 'alerting'); - }) - ); break; case 'global_read at space1': expect(response.body).to.eql({ @@ -339,11 +329,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - await Promise.all( - rules.map((rule) => { - objectRemover.add(space.id, rule.body.id, 'rule', 'alerting'); - }) - ); break; case 'space_1_all_alerts_none_actions at space1': case 'superuser at space1': @@ -367,6 +352,9 @@ export default ({ getService }: FtrProviderContext) => { .expect(200) ) ); + rules.map((rule) => { + objectRemover.add(space.id, rule.body.id, 'rule', 'alerting'); + }); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_disable`) @@ -383,11 +371,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - await Promise.all( - rules.map((rule) => { - objectRemover.add(space.id, rule.body.id, 'rule', 'alerting'); - }) - ); break; case 'global_read at space1': expect(response.body).to.eql({ @@ -396,11 +379,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - await Promise.all( - rules.map((rule) => { - objectRemover.add(space.id, rule.body.id, 'rule', 'alerting'); - }) - ); break; case 'space_1_all_alerts_none_actions at space1': case 'superuser at space1': @@ -408,11 +386,6 @@ export default ({ getService }: FtrProviderContext) => { case 'space_1_all_with_restricted_fixture at space1': expect(response.body.total).to.eql(3); expect(response.statusCode).to.eql(200); - await Promise.all( - rules.map((rule) => { - objectRemover.add(space.id, rule.body.id, 'rule', 'alerting'); - }) - ); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -425,6 +398,7 @@ export default ({ getService }: FtrProviderContext) => { .set('kbn-xsrf', 'foo') .send(getTestRuleData()) .expect(200); + objectRemover.add('other', createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .patch(`${getUrlPrefix('other')}/internal/alerting/rules/_bulk_disable`) @@ -434,10 +408,6 @@ export default ({ getService }: FtrProviderContext) => { switch (scenario.id) { // This superuser has more privileges that we think - case 'superuser at space1': - expect(response.body).to.eql(getDefaultResponse(response)); - expect(response.statusCode).to.eql(200); - break; case 'global_read at space1': expect(response.body).to.eql({ error: 'Forbidden', @@ -445,7 +415,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add('other', createdRule.id, 'rule', 'alerting'); await getScheduledTask(createdRule.scheduled_task_id); break; case 'no_kibana_privileges at space1': @@ -460,7 +429,10 @@ export default ({ getService }: FtrProviderContext) => { }); expect(response.statusCode).to.eql(403); expect(response.statusCode).to.eql(403); - objectRemover.add('other', createdRule.id, 'rule', 'alerting'); + break; + case 'superuser at space1': + expect(response.body).to.eql(getDefaultResponse(response)); + expect(response.statusCode).to.eql(200); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -472,23 +444,23 @@ export default ({ getService }: FtrProviderContext) => { describe('Validation tests', () => { const { user, space } = SuperuserAtSpace1; it('should throw an error when bulk disable of rules when both ids and filter supplied in payload', async () => { - const { body: createdRule1 } = await supertest + const { body: createdRule } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestRuleData({ enabled: true, tags: ['foo'] })) .expect(200); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_disable`) .set('kbn-xsrf', 'foo') - .send({ filter: 'fake_filter', ids: [createdRule1.id] }) + .send({ filter: 'fake_filter', ids: [createdRule.id] }) .auth(user.username, user.password); expect(response.statusCode).to.eql(400); expect(response.body.message).to.eql( "Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method's arguments" ); - objectRemover.add(space.id, createdRule1.id, 'rule', 'alerting'); }); it('should return an error if we pass more than 1000 ids', async () => { @@ -508,11 +480,12 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return an error if we do not pass any arguments', async () => { - await supertest + const { body: createdRule } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestRuleData()) .expect(200); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_disable`) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts index eaabc53cd1dc9..2f5db78b96c5b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts @@ -34,14 +34,13 @@ export default ({ getService }: FtrProviderContext) => { const { user, space } = scenario; describe(scenario.id, () => { - afterEach(() => objectRemover.removeAll()); - it('should handle bulk enable of one rule appropriately based on id', async () => { const { body: createdRule } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send({ ...getTestRuleData(), enabled: false }) .expect(200); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_enable`) @@ -58,7 +57,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'global_read at space1': expect(response.body).to.eql({ @@ -67,7 +65,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'space_1_all_alerts_none_actions at space1': case 'superuser at space1': @@ -126,6 +123,7 @@ export default ({ getService }: FtrProviderContext) => { }) ) .expect(200); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_enable`) @@ -142,7 +140,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'global_read at space1': expect(response.body).to.eql({ @@ -152,7 +149,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': @@ -162,7 +158,6 @@ export default ({ getService }: FtrProviderContext) => { message: 'No rules found for bulk enable', }); expect(response.statusCode).to.eql(400); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -185,6 +180,7 @@ export default ({ getService }: FtrProviderContext) => { }) ) .expect(200); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_enable`) @@ -201,7 +197,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': @@ -213,7 +208,6 @@ export default ({ getService }: FtrProviderContext) => { message: 'No rules found for bulk enable', }); expect(response.statusCode).to.eql(400); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'superuser at space1': expect(response.body).to.eql(defaultSuccessfulResponse); @@ -235,6 +229,7 @@ export default ({ getService }: FtrProviderContext) => { }) ) .expect(200); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_enable`) @@ -251,7 +246,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'global_read at space1': expect(response.body).to.eql({ @@ -260,7 +254,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); break; case 'superuser at space1': case 'space_1_all at space1': @@ -284,6 +277,9 @@ export default ({ getService }: FtrProviderContext) => { .expect(200) ) ); + rules.map((rule) => { + objectRemover.add(space.id, rule.body.id, 'rule', 'alerting'); + }); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_enable`) @@ -300,11 +296,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - await Promise.all( - rules.map((rule) => { - objectRemover.add(space.id, rule.body.id, 'rule', 'alerting'); - }) - ); break; case 'global_read at space1': expect(response.body).to.eql({ @@ -313,11 +304,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - await Promise.all( - rules.map((rule) => { - objectRemover.add(space.id, rule.body.id, 'rule', 'alerting'); - }) - ); break; case 'space_1_all_alerts_none_actions at space1': case 'superuser at space1': @@ -341,6 +327,9 @@ export default ({ getService }: FtrProviderContext) => { .expect(200) ) ); + rules.map((rule) => { + objectRemover.add(space.id, rule.body.id, 'rule', 'alerting'); + }); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_enable`) @@ -357,11 +346,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - await Promise.all( - rules.map((rule) => { - objectRemover.add(space.id, rule.body.id, 'rule', 'alerting'); - }) - ); break; case 'global_read at space1': expect(response.body).to.eql({ @@ -370,11 +354,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - await Promise.all( - rules.map((rule) => { - objectRemover.add(space.id, rule.body.id, 'rule', 'alerting'); - }) - ); break; case 'space_1_all_alerts_none_actions at space1': case 'superuser at space1': @@ -382,11 +361,6 @@ export default ({ getService }: FtrProviderContext) => { case 'space_1_all_with_restricted_fixture at space1': expect(response.body).to.eql({ ...defaultSuccessfulResponse, total: 3 }); expect(response.statusCode).to.eql(200); - await Promise.all( - rules.map((rule) => { - objectRemover.add(space.id, rule.body.id, 'rule', 'alerting'); - }) - ); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -399,6 +373,7 @@ export default ({ getService }: FtrProviderContext) => { .set('kbn-xsrf', 'foo') .send(getTestRuleData()) .expect(200); + objectRemover.add('other', createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .patch(`${getUrlPrefix('other')}/internal/alerting/rules/_bulk_enable`) @@ -419,7 +394,6 @@ export default ({ getService }: FtrProviderContext) => { statusCode: 403, }); expect(response.statusCode).to.eql(403); - objectRemover.add('other', createdRule.id, 'rule', 'alerting'); await getScheduledTask(createdRule.scheduled_task_id); break; case 'no_kibana_privileges at space1': @@ -434,7 +408,6 @@ export default ({ getService }: FtrProviderContext) => { }); expect(response.statusCode).to.eql(403); expect(response.statusCode).to.eql(403); - objectRemover.add('other', createdRule.id, 'rule', 'alerting'); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); @@ -446,23 +419,23 @@ export default ({ getService }: FtrProviderContext) => { describe('Validation tests', () => { const { user, space } = SuperuserAtSpace1; it('should throw an error when bulk enable of rules when both ids and filter supplied in payload', async () => { - const { body: createdRule1 } = await supertest + const { body: createdRule } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestRuleData({ tags: ['foo'] })) .expect(200); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_enable`) .set('kbn-xsrf', 'foo') - .send({ filter: 'fake_filter', ids: [createdRule1.id] }) + .send({ filter: 'fake_filter', ids: [createdRule.id] }) .auth(user.username, user.password); expect(response.statusCode).to.eql(400); expect(response.body.message).to.eql( "Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method's arguments" ); - objectRemover.add(space.id, createdRule1.id, 'rule', 'alerting'); }); it('should return an error if we pass more than 1000 ids', async () => { @@ -482,11 +455,12 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return an error if we do not pass any arguments', async () => { - await supertest + const { body: createdRule } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send(getTestRuleData()) .expect(200); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); const response = await supertestWithoutAuth .patch(`${getUrlPrefix(space.id)}/internal/alerting/rules/_bulk_enable`) From 03b5198f6f80b3dffe0c4a54cf92beca3af3ebad Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 19 Dec 2022 10:37:19 -0500 Subject: [PATCH 13/55] [Fleet] Fix upgrade in package policy editor (#147470) --- .../steps/step_define_package_policy.test.tsx | 43 --- .../steps/step_define_package_policy.tsx | 66 +--- .../single_page_layout/hooks/form.test.tsx | 176 +++++++++ .../single_page_layout/hooks/form.tsx | 63 +++- .../single_page_layout/index.test.tsx | 3 + .../single_page_layout/index.tsx | 25 +- .../hooks/use_history_block.test.tsx | 151 ++++++++ .../hooks/use_history_block.tsx | 50 +++ .../hooks/use_package_policy.test.tsx | 304 +++++++++++++++ .../hooks/use_package_policy.tsx | 354 ++++++++++++++++++ .../edit_package_policy_page/index.test.tsx | 1 + .../edit_package_policy_page/index.tsx | 313 ++-------------- .../public/hooks/use_request/settings.ts | 7 + 13 files changed, 1147 insertions(+), 409 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.test.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.test.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy.test.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.test.tsx index 178d8c8c331df..be9e7425ca617 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.test.tsx @@ -122,49 +122,6 @@ describe('StepDefinePackagePolicy', () => { render(); }); - it('should set index 1 name to package policy on init if no package policies exist for this package', () => { - waitFor(() => { - expect(renderResult.getByDisplayValue('apache-1')).toBeInTheDocument(); - expect(renderResult.getByDisplayValue('desc')).toBeInTheDocument(); - }); - - expect(mockUpdatePackagePolicy.mock.calls[0]).toEqual([ - { - description: 'desc', - enabled: true, - inputs: [], - name: 'apache-1', - namespace: 'default', - policy_id: 'agent-policy-1', - package: { - name: 'apache', - title: 'Apache', - version: '1.0.0', - }, - vars: { - 'Advanced var': { - type: 'bool', - value: true, - }, - 'Required var': { - type: 'bool', - value: undefined, - }, - 'Show user var': { - type: 'string', - value: 'showUserVarVal', - }, - }, - }, - ]); - expect(mockUpdatePackagePolicy.mock.calls[1]).toEqual([ - { - namespace: 'ns', - policy_id: 'agent-policy-1', - }, - ]); - }); - it('should display vars coming from package policy', async () => { waitFor(() => { expect(renderResult.getByDisplayValue('showUserVarVal')).toBeInTheDocument(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.tsx index e7e24dcfb71e6..186dea6c3c3da 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.tsx @@ -29,12 +29,8 @@ import type { NewPackagePolicy, RegistryVarsEntry, } from '../../../../../types'; -import { packageToPackagePolicy, pkgKeyFromPackageInfo } from '../../../../../services'; import { Loading } from '../../../../../components'; -import { useStartServices, useGetPackagePolicies } from '../../../../../hooks'; -import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../../constants'; -import { getMaxPackageName } from '../../../../../../../../common/services'; -import { SO_SEARCH_LIMIT } from '../../../../../../../../common/constants'; +import { useStartServices } from '../../../../../hooks'; import { isAdvancedVar } from '../../services'; import type { PackagePolicyValidationResults } from '../../services'; @@ -74,13 +70,6 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ }) => { const { docLinks } = useStartServices(); - // Fetch all packagePolicies having the package name - const { data: packagePolicyData, isLoading: isLoadingPackagePolicies } = useGetPackagePolicies({ - perPage: SO_SEARCH_LIMIT, - page: 1, - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageInfo.name}`, - }); - // Form show/hide states const [isShowingAdvanced, setIsShowingAdvanced] = useState(noAdvancedToggle); @@ -100,47 +89,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ // Update package policy's package and agent policy info useEffect(() => { - if (isLoadingPackagePolicies) { - return; - } - - if (isUpdate) { - // If we're upgrading, we need to make sure we catch an addition of package-level - // vars when they were previously no package-level vars defined - if (!packagePolicy.vars && packageInfo.vars) { - updatePackagePolicy( - packageToPackagePolicy( - packageInfo, - agentPolicy?.id || '', - packagePolicy.namespace, - packagePolicy.name, - packagePolicy.description, - integrationToEnable - ) - ); - } - } - - const pkg = packagePolicy.package; - const currentPkgKey = pkg ? pkgKeyFromPackageInfo(pkg) : ''; - const pkgKey = pkgKeyFromPackageInfo(packageInfo); - - // If package has changed, create shell package policy with input&stream values based on package info - if (currentPkgKey !== pkgKey) { - const incrementedName = getMaxPackageName(packageInfo.name, packagePolicyData?.items); - - updatePackagePolicy( - packageToPackagePolicy( - packageInfo, - agentPolicy?.id || '', - packagePolicy.namespace, - packagePolicy.name || incrementedName, - packagePolicy.description, - integrationToEnable - ) - ); - } - + // TODO move this to parent hook // If agent policy has changed, update package policy's agent policy ID and namespace if (agentPolicy && packagePolicy.policy_id !== agentPolicy.id) { updatePackagePolicy({ @@ -148,16 +97,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ namespace: agentPolicy.namespace, }); } - }, [ - isUpdate, - packagePolicy, - agentPolicy, - packageInfo, - updatePackagePolicy, - integrationToEnable, - packagePolicyData, - isLoadingPackagePolicies, - ]); + }, [packagePolicy, agentPolicy, packageInfo, updatePackagePolicy]); const isManaged = packagePolicy.is_managed; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.test.tsx new file mode 100644 index 0000000000000..d57b8cff64a84 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.test.tsx @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react-hooks'; + +import type { TestRenderer } from '../../../../../../../mock'; +import { createFleetTestRendererMock } from '../../../../../../../mock'; +import type { PackageInfo } from '../../../../../types'; + +import { sendGetPackagePolicies } from '../../../../../hooks'; + +import { SelectedPolicyTab } from '../../components'; + +import { useOnSubmit } from './form'; + +jest.mock('../../../../../hooks', () => { + return { + ...jest.requireActual('../../../../../hooks'), + sendGetPackagePolicies: jest.fn().mockReturnValue({ + data: { + items: [{ name: 'nginx-1' }, { name: 'other-policy' }], + }, + isLoading: false, + }), + useFleetStatus: jest.fn().mockReturnValue({ isReady: true } as any), + sendGetStatus: jest + .fn() + .mockResolvedValue({ data: { isReady: true, missing_requirements: [] } }), + }; +}); + +describe('useOnSubmit', () => { + const packageInfo: PackageInfo = { + name: 'apache', + version: '1.0.0', + description: '', + format_version: '', + release: 'ga', + owner: { github: '' }, + title: 'Apache', + latestVersion: '', + assets: {} as any, + status: 'not_installed', + vars: [ + { + show_user: true, + name: 'Show user var', + type: 'string', + default: 'showUserVarVal', + }, + { + required: true, + name: 'Required var', + type: 'bool', + }, + { + name: 'Advanced var', + type: 'bool', + default: true, + }, + ], + }; + + let testRenderer: TestRenderer; + let renderResult: RenderHookResult< + Parameters, + ReturnType + >; + const render = ({ isUpdate } = { isUpdate: false }) => + (renderResult = testRenderer.renderHook(() => + useOnSubmit({ + agentCount: 0, + packageInfo, + withSysMonitoring: false, + selectedPolicyTab: SelectedPolicyTab.NEW, + newAgentPolicy: { name: 'test', namespace: 'default' }, + queryParamsPolicyId: undefined, + }) + )); + + beforeEach(() => { + testRenderer = createFleetTestRendererMock(); + }); + + describe('default API response', () => { + beforeEach(() => { + act(() => { + render(); + }); + }); + + it('should set index 1 name to package policy on init if no package policies exist for this package', () => { + // waitFor(() => { + // expect(renderResult.getByDisplayValue('apache-1')).toBeInTheDocument(); + // expect(renderResult.getByDisplayValue('desc')).toBeInTheDocument(); + // }); + + expect(renderResult.result.current.packagePolicy).toEqual({ + description: '', + enabled: true, + inputs: [], + name: 'apache-1', + namespace: 'default', + policy_id: '', + package: { + name: 'apache', + title: 'Apache', + version: '1.0.0', + }, + vars: { + 'Advanced var': { + type: 'bool', + value: true, + }, + 'Required var': { + type: 'bool', + value: undefined, + }, + 'Show user var': { + type: 'string', + value: 'showUserVarVal', + }, + }, + }); + }); + }); + + it('should set incremented name if other package policies exist', async () => { + (sendGetPackagePolicies as jest.MockedFunction).mockReturnValue({ + data: { + items: [ + { name: 'apache-1' }, + { name: 'apache-2' }, + { name: 'apache-9' }, + { name: 'apache-10' }, + ], + }, + isLoading: false, + }); + + await render(); + + expect(renderResult.result.current.packagePolicy).toEqual({ + description: '', + enabled: true, + inputs: [], + name: 'apache-11', + namespace: 'default', + policy_id: '', + package: { + name: 'apache', + title: 'Apache', + version: '1.0.0', + }, + vars: { + 'Advanced var': { + type: 'bool', + value: true, + }, + 'Required var': { + type: 'bool', + value: undefined, + }, + 'Show user var': { + type: 'string', + value: 'showUserVarVal', + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx index e0f206ef612a8..a1b0e2065b3a5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { safeLoad } from 'js-yaml'; @@ -22,9 +22,16 @@ import { sendCreateAgentPolicy, sendCreatePackagePolicy, sendBulkInstallPackages, + sendGetPackagePolicies, } from '../../../../../hooks'; -import { isVerificationError } from '../../../../../services'; -import { FLEET_ELASTIC_AGENT_PACKAGE, FLEET_SYSTEM_PACKAGE } from '../../../../../../../../common'; +import { isVerificationError, packageToPackagePolicy } from '../../../../../services'; +import { + FLEET_ELASTIC_AGENT_PACKAGE, + FLEET_SYSTEM_PACKAGE, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + SO_SEARCH_LIMIT, +} from '../../../../../../../../common'; +import { getMaxPackageName } from '../../../../../../../../common/services'; import { useConfirmForceInstall } from '../../../../../../integrations/hooks'; import { validatePackagePolicy, validationHasErrors } from '../../services'; import type { PackagePolicyValidationResults } from '../../services'; @@ -61,6 +68,15 @@ async function savePackagePolicy(pkgPolicy: CreatePackagePolicyRequest['body']) return result; } +const DEFAULT_PACKAGE_POLICY = { + name: '', + description: '', + namespace: 'default', + policy_id: '', + enabled: true, + inputs: [], +}; + export function useOnSubmit({ agentCount, selectedPolicyTab, @@ -68,6 +84,7 @@ export function useOnSubmit({ withSysMonitoring, queryParamsPolicyId, packageInfo, + integrationToEnable, }: { packageInfo?: PackageInfo; newAgentPolicy: NewAgentPolicy; @@ -75,6 +92,7 @@ export function useOnSubmit({ selectedPolicyTab: SelectedPolicyTab; agentCount: number; queryParamsPolicyId: string | undefined; + integrationToEnable?: string; }) { const { notifications } = useStartServices(); const confirmForceInstall = useConfirmForceInstall(); @@ -83,15 +101,11 @@ export function useOnSubmit({ // Form state const [formState, setFormState] = useState('VALID'); + const isInitializedRef = useRef(false); const [agentPolicy, setAgentPolicy] = useState(); // New package policy state const [packagePolicy, setPackagePolicy] = useState({ - name: '', - description: '', - namespace: 'default', - policy_id: '', - enabled: true, - inputs: [], + ...DEFAULT_PACKAGE_POLICY, }); // Validation state @@ -165,6 +179,36 @@ export function useOnSubmit({ [packagePolicy, setFormState, updatePackagePolicyValidation, selectedPolicyTab] ); + // Initial loading of package info + useEffect(() => { + async function init() { + if (isInitializedRef.current || !packageInfo) { + return; + } + + // Fetch all packagePolicies having the package name + const { data: packagePolicyData } = await sendGetPackagePolicies({ + perPage: SO_SEARCH_LIMIT, + page: 1, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageInfo.name}`, + }); + const incrementedName = getMaxPackageName(packageInfo.name, packagePolicyData?.items); + + isInitializedRef.current = true; + updatePackagePolicy( + packageToPackagePolicy( + packageInfo, + agentPolicy?.id || '', + DEFAULT_PACKAGE_POLICY.namespace, + DEFAULT_PACKAGE_POLICY.name || incrementedName, + DEFAULT_PACKAGE_POLICY.description, + integrationToEnable + ) + ); + } + init(); + }, [packageInfo, agentPolicy, updatePackagePolicy, integrationToEnable]); + const onSaveNavigate = useOnSaveNavigate({ packagePolicy, queryParamsPolicyId, @@ -311,6 +355,7 @@ export function useOnSubmit({ setValidationResults, hasAgentPolicyError, setHasAgentPolicyError, + isInitialized: isInitializedRef.current, // TODO check navigateAddAgent, navigateAddAgentHelp, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx index 0580e89c6c9b4..0cdc581260b53 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx @@ -42,6 +42,9 @@ jest.mock('../../../../hooks', () => { sendGetOneAgentPolicy: jest.fn().mockResolvedValue({ data: { item: { id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' } }, }), + sendGetSettings: jest.fn().mockResolvedValue({ + data: { item: {} }, + }), useGetPackageInfoByKey: jest.fn(), sendCreatePackagePolicy: jest .fn() diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index e1f25b27a7785..2d8baf6b58eb4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -109,6 +109,16 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ const [agentCount, setAgentCount] = useState(0); + const integrationInfo = useMemo( + () => + (params as AddToPolicyParams).integration + ? packageInfo?.policy_templates?.find( + (policyTemplate) => policyTemplate.name === (params as AddToPolicyParams).integration + ) + : undefined, + [packageInfo?.policy_templates, params] + ); + // Save package policy const { onSubmit, @@ -124,6 +134,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ setHasAgentPolicyError, validationResults, hasAgentPolicyError, + isInitialized, } = useOnSubmit({ agentCount, packageInfo, @@ -131,6 +142,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ selectedPolicyTab, withSysMonitoring, queryParamsPolicyId, + integrationToEnable: integrationInfo?.name, }); const setPolicyValidation = useCallback( @@ -215,16 +227,6 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ packageInfo, }); - const integrationInfo = useMemo( - () => - (params as AddToPolicyParams).integration - ? packageInfo?.policy_templates?.find( - (policyTemplate) => policyTemplate.name === (params as AddToPolicyParams).integration - ) - : undefined, - [packageInfo?.policy_templates, params] - ); - const layoutProps = useMemo( () => ({ from, @@ -271,7 +273,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ const stepConfigurePackagePolicy = useMemo( () => - isPackageInfoLoading ? ( + isPackageInfoLoading || !isInitialized ? ( ) : packageInfo ? ( <> @@ -311,6 +313,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
), [ + isInitialized, isPackageInfoLoading, agentPolicy, packageInfo, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.test.tsx new file mode 100644 index 0000000000000..91a4afbda62e2 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.test.tsx @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from '@testing-library/react-hooks'; + +import { createFleetTestRendererMock } from '../../../../../../mock'; + +import { useHistoryBlock } from './use_history_block'; + +describe('useHistoryBlock', () => { + describe('without search params', () => { + it('should not block if not edited', () => { + const renderer = createFleetTestRendererMock(); + + renderer.renderHook(() => useHistoryBlock(false)); + + act(() => renderer.mountHistory.push('/test')); + + const { location } = renderer.mountHistory; + expect(location.pathname).toBe('/test'); + expect(location.search).toBe(''); + expect(renderer.startServices.overlays.openConfirm).not.toBeCalled(); + }); + + it('should block if edited', async () => { + const renderer = createFleetTestRendererMock(); + + renderer.startServices.overlays.openConfirm.mockResolvedValue(true); + renderer.renderHook(() => useHistoryBlock(true)); + + act(() => renderer.mountHistory.push('/test')); + // needed because we have an async useEffect + await act(() => new Promise((resolve) => resolve())); + + expect(renderer.startServices.overlays.openConfirm).toBeCalled(); + expect(renderer.startServices.application.navigateToUrl).toBeCalledWith( + '/mock/test', + expect.anything() + ); + }); + + it('should block if edited and not navigate on cancel', async () => { + const renderer = createFleetTestRendererMock(); + + renderer.startServices.overlays.openConfirm.mockResolvedValue(false); + renderer.renderHook(() => useHistoryBlock(true)); + + act(() => renderer.mountHistory.push('/test')); + // needed because we have an async useEffect + await act(() => new Promise((resolve) => resolve())); + + expect(renderer.startServices.overlays.openConfirm).toBeCalled(); + expect(renderer.startServices.application.navigateToUrl).not.toBeCalled(); + }); + }); + describe('with search params', () => { + it('should not block if not edited', () => { + const renderer = createFleetTestRendererMock(); + + renderer.renderHook(() => useHistoryBlock(false)); + + act(() => renderer.mountHistory.push('/test?param=test')); + + const { location } = renderer.mountHistory; + expect(location.pathname).toBe('/test'); + expect(location.search).toBe('?param=test'); + expect(renderer.startServices.overlays.openConfirm).not.toBeCalled(); + }); + + it('should block if edited and navigate on confirm', async () => { + const renderer = createFleetTestRendererMock(); + + renderer.startServices.overlays.openConfirm.mockResolvedValue(true); + renderer.renderHook(() => useHistoryBlock(true)); + + act(() => renderer.mountHistory.push('/test?param=test')); + // needed because we have an async useEffect + await act(() => new Promise((resolve) => resolve())); + + expect(renderer.startServices.overlays.openConfirm).toBeCalled(); + expect(renderer.startServices.application.navigateToUrl).toBeCalledWith( + '/mock/test?param=test', + expect.anything() + ); + }); + + it('should block if edited and not navigate on cancel', async () => { + const renderer = createFleetTestRendererMock(); + + renderer.startServices.overlays.openConfirm.mockResolvedValue(false); + renderer.renderHook(() => useHistoryBlock(true)); + + act(() => renderer.mountHistory.push('/test?param=test')); + // needed because we have an async useEffect + await act(() => new Promise((resolve) => resolve())); + + expect(renderer.startServices.overlays.openConfirm).toBeCalled(); + expect(renderer.startServices.application.navigateToUrl).not.toBeCalled(); + }); + }); + + describe('with hash params', () => { + it('should not block if not edited', () => { + const renderer = createFleetTestRendererMock(); + + renderer.renderHook(() => useHistoryBlock(false)); + + act(() => renderer.mountHistory.push('/test#/hash')); + + const { location } = renderer.mountHistory; + expect(location.pathname).toBe('/test'); + expect(location.hash).toBe('#/hash'); + expect(renderer.startServices.overlays.openConfirm).not.toBeCalled(); + }); + + it('should block if edited and navigate on confirm', async () => { + const renderer = createFleetTestRendererMock(); + + renderer.startServices.overlays.openConfirm.mockResolvedValue(true); + renderer.renderHook(() => useHistoryBlock(true)); + + act(() => renderer.mountHistory.push('/test#/hash')); + // needed because we have an async useEffect + await act(() => new Promise((resolve) => resolve())); + + expect(renderer.startServices.overlays.openConfirm).toBeCalled(); + expect(renderer.startServices.application.navigateToUrl).toBeCalledWith( + '/mock/test#/hash', + expect.anything() + ); + }); + + it('should block if edited and not navigate on cancel', async () => { + const renderer = createFleetTestRendererMock(); + + renderer.startServices.overlays.openConfirm.mockResolvedValue(false); + renderer.renderHook(() => useHistoryBlock(true)); + + act(() => renderer.mountHistory.push('/test#/hash')); + // needed because we have an async useEffect + await act(() => new Promise((resolve) => resolve())); + + expect(renderer.startServices.overlays.openConfirm).toBeCalled(); + expect(renderer.startServices.application.navigateToUrl).not.toBeCalled(); + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.tsx new file mode 100644 index 0000000000000..edf04f8733ad8 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.tsx @@ -0,0 +1,50 @@ +/* + * 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 { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; + +import { useStartServices } from '../../../../hooks'; + +export function useHistoryBlock(isEdited: boolean) { + const history = useHistory(); + const { overlays, application } = useStartServices(); + + useEffect(() => { + if (!isEdited) { + return; + } + + const unblock = history.block((state) => { + async function confirmAsync() { + const confirmRes = await overlays.openConfirm( + i18n.translate('xpack.fleet.editPackagePolicy.historyBlockDescription', { + defaultMessage: `Unsaved changes will be discarded. Are you sure you would like to continue?`, + }), + { + title: i18n.translate('xpack.fleet.editPackagePolicy.historyBlockTitle', { + defaultMessage: 'Discard Changes?', + }), + } + ); + + if (confirmRes) { + unblock(); + + application.navigateToUrl(state.pathname + state.hash + state.search, { + state: state.state, + }); + } + } + confirmAsync(); + return false; + }); + + return unblock; + }, [history, isEdited, overlays, application]); +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy.test.tsx new file mode 100644 index 0000000000000..ef5d2efcf53ac --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy.test.tsx @@ -0,0 +1,304 @@ +/* + * 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. + */ + +/* + * 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 { omit } from 'lodash'; + +import { createFleetTestRendererMock } from '../../../../../../mock'; + +import { usePackagePolicyWithRelatedData } from './use_package_policy'; + +const mockPackagePolicy = { + id: 'nginx-1', + name: 'nginx-1', + namespace: 'default', + description: 'Nginx description', + package: { name: 'nginx', title: 'Nginx', version: '1.3.0' }, + enabled: true, + policy_id: 'agent-policy-1', + vars: {}, + inputs: [ + { + type: 'logfile', + policy_template: 'nginx', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'nginx.access' }, + vars: { + paths: { value: ['/var/log/nginx/access.log*'], type: 'text' }, + }, + }, + ], + vars: undefined, + }, + ], +}; + +jest.mock('../../../../../../hooks/use_request', () => ({ + ...jest.requireActual('../../../../../../hooks/use_request'), + sendGetOnePackagePolicy: (packagePolicyId: string) => { + if (packagePolicyId === 'package-policy-1') { + return { + data: { + item: { + id: 'nginx-1', + name: 'nginx-1', + namespace: 'default', + description: 'Nginx description', + package: { name: 'nginx', title: 'Nginx', version: '1.3.0' }, + enabled: true, + policy_id: 'agent-policy-1', + inputs: [ + { + type: 'logfile', + policy_template: 'nginx', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'nginx.access' }, + vars: { + paths: { value: ['/var/log/nginx/access.log*'], type: 'text' }, + }, + }, + ], + vars: undefined, + }, + ], + }, + }, + }; + } + }, + sendGetPackageInfoByKey: jest.fn().mockImplementation((name, version) => + Promise.resolve({ + data: { + item: { + name, + title: 'Nginx', + version, + release: 'ga', + description: 'Collect logs and metrics from Nginx HTTP servers with Elastic Agent.', + policy_templates: [ + { + name: 'nginx', + title: 'Nginx logs and metrics', + description: 'Collect logs and metrics from Nginx instances', + inputs: [ + { + type: 'logfile', + title: 'Collect logs from Nginx instances', + description: 'Collecting Nginx access and error logs', + vars: [ + { + name: 'new_input_level_var', + type: 'text', + title: 'Paths', + required: false, + show_user: true, + }, + ], + }, + ], + multiple: true, + }, + ], + data_streams: [ + { + type: 'logs', + dataset: 'nginx.access', + title: 'Nginx access logs', + release: 'experimental', + ingest_pipeline: 'default', + streams: [ + { + input: 'logfile', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: true, + required: true, + show_user: true, + default: ['/var/log/nginx/access.log*'], + }, + ], + template_path: 'stream.yml.hbs', + title: 'Nginx access logs', + description: 'Collect Nginx access logs', + enabled: true, + }, + ], + package: 'nginx', + path: 'access', + }, + ], + latestVersion: version, + keepPoliciesUpToDate: false, + status: 'not_installed', + vars: [ + { + name: 'new_package_level_var', + type: 'text', + title: 'Paths', + required: false, + show_user: true, + }, + ], + }, + }, + isLoading: false, + }) + ), + sendUpgradePackagePolicyDryRun: jest.fn().mockResolvedValue({ + data: [ + { + diff: [ + { + id: 'nginx-1', + name: 'nginx-1', + namespace: 'default', + description: 'Nginx description', + package: { name: 'nginx', title: 'Nginx', version: '1.3.0' }, + enabled: true, + policy_id: 'agent-policy-1', + vars: {}, + inputs: [ + { + type: 'logfile', + policy_template: 'nginx', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'nginx.access' }, + vars: { + paths: { value: ['/var/log/nginx/access.log*'], type: 'text' }, + }, + }, + ], + vars: undefined, + }, + ], + }, + { + id: 'nginx-1', + name: 'nginx-1', + namespace: 'default', + description: 'Nginx description', + package: { name: 'nginx', title: 'Nginx', version: '1.4.0' }, + enabled: true, + policy_id: 'agent-policy-1', + vars: { + new_package_level_var: { value: 'test', type: 'text' }, + }, + inputs: [ + { + type: 'logfile', + policy_template: 'nginx', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'nginx.access' }, + vars: { + paths: { value: ['/var/log/nginx/access.log*'], type: 'text' }, + }, + }, + ], + vars: { + new_input_level_var: { value: 'test', type: 'text' }, + }, + }, + ], + }, + ], + }, + ], + }), +})); + +describe('usePackagePolicy', () => { + it('should load the package policy if this is a not an upgrade', async () => { + const renderer = createFleetTestRendererMock(); + const { result, waitForNextUpdate } = renderer.renderHook(() => + usePackagePolicyWithRelatedData('package-policy-1', {}) + ); + await waitForNextUpdate(); + + expect(result.current.packagePolicy).toEqual(omit(mockPackagePolicy, 'id')); + }); + + it('should load the package policy if this is an upgrade', async () => { + const renderer = createFleetTestRendererMock(); + const { result, waitForNextUpdate } = renderer.renderHook(() => + usePackagePolicyWithRelatedData('package-policy-1', { + forceUpgrade: true, + }) + ); + await waitForNextUpdate(); + expect(result.current.packagePolicy).toMatchInlineSnapshot(` + Object { + "description": "Nginx description", + "enabled": true, + "inputs": Array [ + Object { + "enabled": true, + "policy_template": "nginx", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "nginx.access", + "type": "logs", + }, + "enabled": true, + "vars": Object { + "paths": Object { + "type": "text", + "value": Array [ + "/var/log/nginx/access.log*", + ], + }, + }, + }, + ], + "type": "logfile", + "vars": Object { + "new_input_level_var": Object { + "type": "text", + "value": "test", + }, + }, + }, + ], + "name": "nginx-1", + "namespace": "default", + "package": Object { + "name": "nginx", + "title": "Nginx", + "version": "1.4.0", + }, + "policy_id": "agent-policy-1", + "vars": Object { + "new_package_level_var": Object { + "type": "text", + "value": "test", + }, + }, + } + `); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy.tsx new file mode 100644 index 0000000000000..34f30e169cd97 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy.tsx @@ -0,0 +1,354 @@ +/* + * 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 { useCallback, useEffect, useState } from 'react'; +import { safeLoad } from 'js-yaml'; +import deepEqual from 'fast-deep-equal'; +import { pick } from 'lodash'; + +import type { + GetOnePackagePolicyResponse, + UpgradePackagePolicyDryRunResponse, +} from '../../../../../../../common/types/rest_spec'; +import { + sendGetOneAgentPolicy, + sendGetOnePackagePolicy, + sendGetPackageInfoByKey, + sendGetSettings, + sendUpdatePackagePolicy, + sendUpgradePackagePolicyDryRun, +} from '../../../../hooks'; +import type { + PackagePolicyConfigRecord, + UpdatePackagePolicy, + AgentPolicy, + PackagePolicy, + PackageInfo, +} from '../../../../types'; +import { + type PackagePolicyValidationResults, + validatePackagePolicy, + validationHasErrors, +} from '../../create_package_policy_page/services'; +import type { PackagePolicyFormState } from '../../create_package_policy_page/types'; +import { fixApmDurationVars, hasUpgradeAvailable } from '../utils'; + +function mergeVars( + packageVars?: PackagePolicyConfigRecord, + userVars: PackagePolicyConfigRecord = {} +): PackagePolicyConfigRecord { + if (!packageVars) { + return {}; + } + + return Object.entries(packageVars).reduce((acc, [varKey, varRecord]) => { + acc[varKey] = { + ...varRecord, + value: userVars?.[varKey]?.value ?? varRecord.value, + }; + + return acc; + }, {} as PackagePolicyConfigRecord); +} + +async function isPreleaseEnabled() { + const { data: settings } = await sendGetSettings(); + + return Boolean(settings?.item.prerelease_integrations_enabled); +} + +export function usePackagePolicyWithRelatedData( + packagePolicyId: string, + options: { + forceUpgrade?: boolean; + } +) { + const [packageInfo, setPackageInfo] = useState(); + const [packagePolicy, setPackagePolicy] = useState({ + name: '', + description: '', + namespace: '', + policy_id: '', + enabled: true, + inputs: [], + version: '', + }); + const [originalPackagePolicy, setOriginalPackagePolicy] = + useState(); + const [agentPolicy, setAgentPolicy] = useState(); + const [isLoadingData, setIsLoadingData] = useState(true); + const [dryRunData, setDryRunData] = useState(); + const [loadingError, setLoadingError] = useState(); + + const [isUpgrade, setIsUpgrade] = useState(options.forceUpgrade ?? false); + + // Form state + const [isEdited, setIsEdited] = useState(false); + const [formState, setFormState] = useState('INVALID'); + const [validationResults, setValidationResults] = useState(); + const hasErrors = validationResults ? validationHasErrors(validationResults) : false; + + const savePackagePolicy = async () => { + setFormState('LOADING'); + const { elasticsearch, ...restPackagePolicy } = packagePolicy; // ignore 'elasticsearch' property since it fails route validation + const result = await sendUpdatePackagePolicy(packagePolicyId, restPackagePolicy); + setFormState('SUBMITTED'); + return result; + }; + // Update package policy validation + const updatePackagePolicyValidation = useCallback( + (newPackagePolicy?: UpdatePackagePolicy) => { + if (packageInfo) { + const newValidationResult = validatePackagePolicy( + newPackagePolicy || packagePolicy, + packageInfo, + safeLoad + ); + setValidationResults(newValidationResult); + // eslint-disable-next-line no-console + console.debug('Package policy validation results', newValidationResult); + + return newValidationResult; + } + }, + [packagePolicy, packageInfo] + ); + // Update package policy method + const updatePackagePolicy = useCallback( + (updatedFields: Partial) => { + const isDeepEqual = deepEqual( + JSON.parse(JSON.stringify(updatedFields)), + JSON.parse(JSON.stringify(pick(packagePolicy, Object.keys(updatedFields)))) + ); + + if (!isDeepEqual) { + setIsEdited(true); + } + + const newPackagePolicy = { + ...packagePolicy, + ...updatedFields, + }; + setPackagePolicy(newPackagePolicy); + + // eslint-disable-next-line no-console + console.debug('Package policy updated', newPackagePolicy); + const newValidationResults = updatePackagePolicyValidation(newPackagePolicy); + const hasValidationErrors = newValidationResults + ? validationHasErrors(newValidationResults) + : false; + if (!hasValidationErrors) { + setFormState('VALID'); + } else { + setFormState('INVALID'); + } + }, + [packagePolicy, updatePackagePolicyValidation] + ); + + // Load the package policy and related data + useEffect(() => { + const getData = async () => { + setIsLoadingData(true); + setLoadingError(undefined); + try { + const prerelease = await isPreleaseEnabled(); + + const { data: packagePolicyData, error: packagePolicyError } = + await sendGetOnePackagePolicy(packagePolicyId); + + if (packagePolicyError) { + throw packagePolicyError; + } + + const { data: agentPolicyData, error: agentPolicyError } = await sendGetOneAgentPolicy( + packagePolicyData!.item.policy_id + ); + + if (agentPolicyError) { + throw agentPolicyError; + } + + if (agentPolicyData?.item) { + setAgentPolicy(agentPolicyData.item); + } + + const { data: upgradePackagePolicyDryRunData, error: upgradePackagePolicyDryRunError } = + await sendUpgradePackagePolicyDryRun([packagePolicyId]); + + if (upgradePackagePolicyDryRunError) { + throw upgradePackagePolicyDryRunError; + } + + const hasUpgrade = upgradePackagePolicyDryRunData + ? hasUpgradeAvailable(upgradePackagePolicyDryRunData) + : false; + + const isUpgradeScenario = options.forceUpgrade && hasUpgrade; + // If the dry run data doesn't indicate a difference in version numbers, flip the form back + // to its non-upgrade state, even if we were initially set to the upgrade view + if (!hasUpgrade) { + setIsUpgrade(false); + } + + if (upgradePackagePolicyDryRunData && hasUpgrade) { + setDryRunData(upgradePackagePolicyDryRunData); + } + + const basePolicy: PackagePolicy | undefined = packagePolicyData?.item; + let baseInputs: any = basePolicy?.inputs; + let basePackage: any = basePolicy?.package; + let baseVars = basePolicy?.vars; + + const proposedUpgradePackagePolicy = upgradePackagePolicyDryRunData?.[0]?.diff?.[1]; + + if (isUpgradeScenario) { + if (!proposedUpgradePackagePolicy) { + throw new Error( + 'There was an error when trying to load upgrade diff for that package policy' + ); + } + // If we're upgrading the package, we need to "start from" the policy as it's returned from + // the dry run so we can allow the user to edit any new variables before saving + upgrading + baseInputs = proposedUpgradePackagePolicy.inputs; + basePackage = proposedUpgradePackagePolicy.package; + baseVars = proposedUpgradePackagePolicy.vars; + } + + if (basePolicy) { + setOriginalPackagePolicy(basePolicy); + + const { + id, + revision, + inputs, + vars, + /* eslint-disable @typescript-eslint/naming-convention */ + created_by, + created_at, + updated_by, + updated_at, + /* eslint-enable @typescript-eslint/naming-convention */ + ...restOfPackagePolicy + } = basePolicy; + + // const newVars = baseVars; + + // Remove `compiled_stream` from all stream info, we assign this after saving + const newPackagePolicy: UpdatePackagePolicy = { + ...restOfPackagePolicy, + // If we're upgrading, we need to make sure we catch an addition of package-level + // vars when they were previously no package-level vars defined + vars: mergeVars(baseVars, vars), + inputs: baseInputs.map((input: any) => { + // Remove `compiled_input` from all input info, we assign this after saving + const { + streams, + compiled_input: compiledInput, + vars: inputVars, + ...restOfInput + } = input; + + let basePolicyInputVars: any = + isUpgradeScenario && + basePolicy.inputs.find( + (i) => i.type === input.type && i.policy_template === input.policy_template + )?.vars; + let newInputVars = inputVars; + if (basePolicyInputVars && inputVars) { + // merging vars from dry run with updated ones + basePolicyInputVars = Object.keys(inputVars).reduce( + (acc, curr) => ({ ...acc, [curr]: basePolicyInputVars[curr] }), + {} + ); + newInputVars = { + ...inputVars, + ...basePolicyInputVars, + }; + } + // Fix duration vars, if it's a migrated setting, and it's a plain old number with no suffix + if (basePackage.name === 'apm') { + newInputVars = fixApmDurationVars(newInputVars); + } + return { + ...restOfInput, + streams: streams.map((stream: any) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { compiled_stream, ...restOfStream } = stream; + return restOfStream; + }), + vars: newInputVars, + }; + }), + package: basePackage, + }; + + setPackagePolicy(newPackagePolicy); + + if (basePolicy.package) { + let _packageInfo = basePolicy.package; + + // When upgrading, we need to grab the `packageInfo` data from the new package version's + // proposed policy (comes from the dry run diff) to ensure we have the valid package key/version + // before saving + if (isUpgradeScenario && !!upgradePackagePolicyDryRunData?.[0]?.diff?.[1]?.package) { + _packageInfo = upgradePackagePolicyDryRunData[0].diff?.[1]?.package; + } + + const { data: packageData } = await sendGetPackageInfoByKey( + _packageInfo!.name, + _packageInfo!.version, + { prerelease, full: true } + ); + + if (packageData?.item) { + setPackageInfo(packageData.item); + + const newValidationResults = validatePackagePolicy( + newPackagePolicy, + packageData.item, + safeLoad + ); + setValidationResults(newValidationResults); + + if (validationHasErrors(newValidationResults)) { + setFormState('INVALID'); + } else { + setFormState('VALID'); + } + } + } + } + } catch (e) { + setLoadingError(e); + } + setIsLoadingData(false); + }; + getData(); + }, [packagePolicyId, options.forceUpgrade]); + + return { + // form + formState, + validationResults, + hasErrors, + upgradeDryRunData: dryRunData, + setFormState, + updatePackagePolicy, + isEdited, + setIsEdited, + // data + packageInfo, + isUpgrade, + savePackagePolicy, + isLoadingData, + agentPolicy, + loadingError, + packagePolicy, + originalPackagePolicy, + }; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.test.tsx index df42a287ffa16..c295cdd41cecb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.test.tsx @@ -263,6 +263,7 @@ describe('edit package policy page', () => { const { id, ...restProps } = mockPackagePolicy; expect(sendUpdatePackagePolicy).toHaveBeenCalledWith('nginx-1', { ...restProps, + vars: {}, inputs: [ { ...mockPackagePolicy.inputs[0], diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index c916976a7d094..07e0636f04640 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -6,12 +6,10 @@ */ import React, { useState, useEffect, useCallback, useMemo, memo } from 'react'; -import { pick, omit } from 'lodash'; +import { omit } from 'lodash'; import { useRouteMatch } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { safeLoad } from 'js-yaml'; -import deepEqual from 'fast-deep-equal'; import { EuiButtonEmpty, EuiBottomBar, @@ -21,21 +19,15 @@ import { EuiErrorBoundary, } from '@elastic/eui'; -import type { AgentPolicy, PackageInfo, UpdatePackagePolicy, PackagePolicy } from '../../../types'; +import type { PackageInfo } from '../../../types'; import { useLink, useBreadcrumbs, useStartServices, useConfig, useUIExtension, - sendUpdatePackagePolicy, sendGetAgentStatus, - sendGetOneAgentPolicy, - sendGetOnePackagePolicy, - sendGetPackageInfoByKey, - sendUpgradePackagePolicyDryRun, useAuthz, - useGetSettings, } from '../../../hooks'; import { useBreadcrumbs as useIntegrationsBreadcrumbs, @@ -50,28 +42,20 @@ import { } from '../../../components'; import { ConfirmDeployAgentPolicyModal } from '../components'; import { CreatePackagePolicySinglePageLayout } from '../create_package_policy_page/single_page_layout/components'; -import type { PackagePolicyValidationResults } from '../create_package_policy_page/services'; -import { validatePackagePolicy, validationHasErrors } from '../create_package_policy_page/services'; -import type { - PackagePolicyFormState, - EditPackagePolicyFrom, -} from '../create_package_policy_page/types'; +import type { EditPackagePolicyFrom } from '../create_package_policy_page/types'; import { StepConfigurePackagePolicy, StepDefinePackagePolicy, } from '../create_package_policy_page/components'; -import type { - GetOnePackagePolicyResponse, - UpgradePackagePolicyDryRunResponse, -} from '../../../../../../common/types/rest_spec'; + import { HIDDEN_API_REFERENCE_PACKAGES } from '../../../../../../common/constants'; import type { PackagePolicyEditExtensionComponentProps } from '../../../types'; import { ExperimentalFeaturesService, pkgKeyFromPackageInfo } from '../../../services'; import { generateUpdatePackagePolicyDevToolsRequest } from '../services'; -import { fixApmDurationVars, hasUpgradeAvailable } from './utils'; import { useHistoryBlock } from './hooks'; import { UpgradeStatusCallout } from './components'; +import { usePackagePolicyWithRelatedData } from './hooks/use_package_policy'; export const EditPackagePolicyPage = memo(() => { const { @@ -108,205 +92,34 @@ export const EditPackagePolicyForm = memo<{ } = useConfig(); const { getHref } = useLink(); - // Agent policy, package info, and package policy states - const [isLoadingData, setIsLoadingData] = useState(true); - const [loadingError, setLoadingError] = useState(); - const [agentPolicy, setAgentPolicy] = useState(); - const [packageInfo, setPackageInfo] = useState(); - const [packagePolicy, setPackagePolicy] = useState({ - name: '', - description: '', - namespace: '', - policy_id: '', - enabled: true, - inputs: [], - version: '', + const [] = useState(); + const { + // data + agentPolicy, + isLoadingData, + loadingError, + packagePolicy, + originalPackagePolicy, + packageInfo, + upgradeDryRunData, + // form + formState, + setFormState, + isUpgrade, + isEdited, + setIsEdited, + savePackagePolicy, + hasErrors, + updatePackagePolicy, + validationResults, + } = usePackagePolicyWithRelatedData(packagePolicyId, { + forceUpgrade, }); - const [originalPackagePolicy, setOriginalPackagePolicy] = - useState(); - const [dryRunData, setDryRunData] = useState(); - - const [isUpgrade, setIsUpgrade] = useState(false); const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies; - const [prerelease, setPrerelease] = React.useState(false); - - const { data: settings } = useGetSettings(); - - useEffect(() => { - const isEnabled = Boolean(settings?.item.prerelease_integrations_enabled); - if (settings?.item) { - setPrerelease(isEnabled); - } - }, [settings?.item]); - - useEffect(() => { - if (forceUpgrade) { - setIsUpgrade(true); - } - }, [forceUpgrade]); - const policyId = agentPolicy?.id ?? ''; - // Retrieve agent policy, package, and package policy info - useEffect(() => { - const getData = async () => { - setIsLoadingData(true); - setLoadingError(undefined); - try { - const { data: packagePolicyData, error: packagePolicyError } = - await sendGetOnePackagePolicy(packagePolicyId); - - if (packagePolicyError) { - throw packagePolicyError; - } - - const { data: agentPolicyData, error: agentPolicyError } = await sendGetOneAgentPolicy( - packagePolicyData!.item.policy_id - ); - - if (agentPolicyError) { - throw agentPolicyError; - } - - if (agentPolicyData?.item) { - setAgentPolicy(agentPolicyData.item); - } - - const { data: upgradePackagePolicyDryRunData, error: upgradePackagePolicyDryRunError } = - await sendUpgradePackagePolicyDryRun([packagePolicyId]); - - if (upgradePackagePolicyDryRunError) { - throw upgradePackagePolicyDryRunError; - } - - const hasUpgrade = upgradePackagePolicyDryRunData - ? hasUpgradeAvailable(upgradePackagePolicyDryRunData) - : false; - - // If the dry run data doesn't indicate a difference in version numbers, flip the form back - // to its non-upgrade state, even if we were initially set to the upgrade view - if (!hasUpgrade) { - setIsUpgrade(false); - } - - if (upgradePackagePolicyDryRunData && hasUpgrade) { - setDryRunData(upgradePackagePolicyDryRunData); - } - - const basePolicy: PackagePolicy | undefined = packagePolicyData?.item; - let baseInputs: any = basePolicy?.inputs; - let basePackage: any = basePolicy?.package; - - const proposedUpgradePackagePolicy = upgradePackagePolicyDryRunData?.[0]?.diff?.[1]; - - // If we're upgrading the package, we need to "start from" the policy as it's returned from - // the dry run so we can allow the user to edit any new variables before saving + upgrading - if (isUpgrade && !!proposedUpgradePackagePolicy) { - baseInputs = proposedUpgradePackagePolicy.inputs; - basePackage = proposedUpgradePackagePolicy.package; - } - - if (basePolicy) { - setOriginalPackagePolicy(basePolicy); - - const { - id, - revision, - inputs, - /* eslint-disable @typescript-eslint/naming-convention */ - created_by, - created_at, - updated_by, - updated_at, - /* eslint-enable @typescript-eslint/naming-convention */ - ...restOfPackagePolicy - } = basePolicy as any; - // Remove `compiled_stream` from all stream info, we assign this after saving - const newPackagePolicy = { - ...restOfPackagePolicy, - inputs: baseInputs.map((input: any) => { - // Remove `compiled_input` from all input info, we assign this after saving - const { streams, compiled_input: compiledInput, vars, ...restOfInput } = input; - let basePolicyInputVars: any = - isUpgrade && - basePolicy.inputs.find( - (i) => i.type === input.type && i.policy_template === input.policy_template - )?.vars; - let newVars = vars; - if (basePolicyInputVars && vars) { - // merging vars from dry run with updated ones - basePolicyInputVars = Object.keys(vars).reduce( - (acc, curr) => ({ ...acc, [curr]: basePolicyInputVars[curr] }), - {} - ); - newVars = { - ...vars, - ...basePolicyInputVars, - }; - } - // Fix duration vars, if it's a migrated setting, and it's a plain old number with no suffix - if (basePackage.name === 'apm') { - newVars = fixApmDurationVars(newVars); - } - return { - ...restOfInput, - streams: streams.map((stream: any) => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { compiled_stream, ...restOfStream } = stream; - return restOfStream; - }), - vars: newVars, - }; - }), - package: basePackage, - }; - - setPackagePolicy(newPackagePolicy); - - if (basePolicy.package) { - let _packageInfo = basePolicy.package; - - // When upgrading, we need to grab the `packageInfo` data from the new package version's - // proposed policy (comes from the dry run diff) to ensure we have the valid package key/version - // before saving - if (isUpgrade && !!upgradePackagePolicyDryRunData?.[0]?.diff?.[1]?.package) { - _packageInfo = upgradePackagePolicyDryRunData[0].diff?.[1]?.package; - } - - const { data: packageData } = await sendGetPackageInfoByKey( - _packageInfo!.name, - _packageInfo!.version, - { prerelease, full: true } - ); - - if (packageData?.item) { - setPackageInfo(packageData.item); - - const newValidationResults = validatePackagePolicy( - newPackagePolicy, - packageData.item, - safeLoad - ); - setValidationResults(newValidationResults); - - if (validationHasErrors(newValidationResults)) { - setFormState('INVALID'); - } else { - setFormState('VALID'); - } - } - } - } - } catch (e) { - setLoadingError(e); - } - setIsLoadingData(false); - }; - getData(); - }, [policyId, packagePolicyId, isUpgrade, prerelease]); - // Retrieve agent count const [agentCount, setAgentCount] = useState(0); useEffect(() => { @@ -322,61 +135,6 @@ export const EditPackagePolicyForm = memo<{ } }, [policyId, isFleetEnabled]); - // Package policy validation state - const [validationResults, setValidationResults] = useState(); - const hasErrors = validationResults ? validationHasErrors(validationResults) : false; - - // Update package policy validation - const updatePackagePolicyValidation = useCallback( - (newPackagePolicy?: UpdatePackagePolicy) => { - if (packageInfo) { - const newValidationResult = validatePackagePolicy( - newPackagePolicy || packagePolicy, - packageInfo, - safeLoad - ); - setValidationResults(newValidationResult); - // eslint-disable-next-line no-console - console.debug('Package policy validation results', newValidationResult); - - return newValidationResult; - } - }, - [packagePolicy, packageInfo] - ); - - // Update package policy method - const updatePackagePolicy = useCallback( - (updatedFields: Partial) => { - const isDeepEqual = deepEqual( - JSON.parse(JSON.stringify(updatedFields)), - JSON.parse(JSON.stringify(pick(packagePolicy, Object.keys(updatedFields)))) - ); - if (!isDeepEqual) { - setIsEdited(true); - } - - const newPackagePolicy = { - ...packagePolicy, - ...updatedFields, - }; - setPackagePolicy(newPackagePolicy); - - // eslint-disable-next-line no-console - console.debug('Package policy updated', newPackagePolicy); - const newValidationResults = updatePackagePolicyValidation(newPackagePolicy); - const hasValidationErrors = newValidationResults - ? validationHasErrors(newValidationResults) - : false; - if (!hasValidationErrors) { - setFormState('VALID'); - } else { - setFormState('INVALID'); - } - }, - [packagePolicy, updatePackagePolicyValidation] - ); - const handleExtensionViewOnChange = useCallback< PackagePolicyEditExtensionComponentProps['onChange'] >( @@ -389,7 +147,7 @@ export const EditPackagePolicyForm = memo<{ return prevState; }); }, - [updatePackagePolicy] + [updatePackagePolicy, setFormState] ); // Cancel url + Success redirect Path: @@ -417,17 +175,6 @@ export const EditPackagePolicyForm = memo<{ return '/'; }, [from, getHref, packageInfo, policyId]); - // Save package policy - const [isEdited, setIsEdited] = useState(false); - const [formState, setFormState] = useState('INVALID'); - const savePackagePolicy = async () => { - setFormState('LOADING'); - const { elasticsearch, ...restPackagePolicy } = packagePolicy; // ignore 'elasticsearch' property since it fails route validation - const result = await sendUpdatePackagePolicy(packagePolicyId, restPackagePolicy); - setFormState('SUBMITTED'); - return result; - }; - useHistoryBlock(isEdited); const onSubmit = async () => { @@ -645,9 +392,9 @@ export const EditPackagePolicyForm = memo<{ onCancel={() => setFormState('VALID')} /> )} - {isUpgrade && dryRunData && ( + {isUpgrade && upgradeDryRunData && ( <> - + )} diff --git a/x-pack/plugins/fleet/public/hooks/use_request/settings.ts b/x-pack/plugins/fleet/public/hooks/use_request/settings.ts index 83b2612822f60..fdd6e87bdc02f 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/settings.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/settings.ts @@ -17,6 +17,13 @@ export function useGetSettings() { }); } +export function sendGetSettings() { + return sendRequest({ + method: 'get', + path: settingsRoutesService.getInfoPath(), + }); +} + export function sendPutSettings(body: PutSettingsRequest['body']) { return sendRequest({ method: 'put', From 5568ad86df4db0e9c477511f0b9f43fdd920194b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Mon, 19 Dec 2022 16:45:28 +0100 Subject: [PATCH 14/55] [core-http] `net.socket.timeout` is now documented (#147754) --- .../http/core-http-router-server-internal/src/request.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/http/core-http-router-server-internal/src/request.ts b/packages/core/http/core-http-router-server-internal/src/request.ts index 49e764a50cc00..243ea5acb78ce 100644 --- a/packages/core/http/core-http-router-server-internal/src/request.ts +++ b/packages/core/http/core-http-router-server-internal/src/request.ts @@ -176,9 +176,8 @@ export class CoreKibanaRequest< timeout: payloadTimeout, } = request.route.settings.payload || {}; - // net.Socket#timeout isn't documented, yet, and isn't part of the types... https://github.com/nodejs/node/pull/34543 - // the socket is also undefined when using @hapi/shot, or when a "fake request" is used - const socketTimeout = (request.raw.req.socket as any)?.timeout; + // the socket is undefined when using @hapi/shot, or when a "fake request" is used + const socketTimeout = request.raw.req.socket?.timeout; const options = { authRequired: this.getAuthRequired(request), // TypeScript note: Casting to `RouterOptions` to fix the following error: From 049d8021eb3cce6614a86a2904bc8d285f5ce028 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Mon, 19 Dec 2022 09:19:26 -0700 Subject: [PATCH 15/55] Updates upgrade assistant doclinks to point to current rather than hard-coded 7.17 (#147585) Co-authored-by: Lisa Cawley Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Fix https://github.com/elastic/kibana/issues/145092 --- docs/management/upgrade-assistant.asciidoc | 29 +++++++++++++++++++ docs/redirects.asciidoc | 7 +---- .../resolving-migration-failures.asciidoc | 2 +- .../upgrade/saved-objects-migration.asciidoc | 2 +- docs/setup/upgrade/upgrade-standard.asciidoc | 3 +- docs/user/management.asciidoc | 2 ++ 6 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 docs/management/upgrade-assistant.asciidoc diff --git a/docs/management/upgrade-assistant.asciidoc b/docs/management/upgrade-assistant.asciidoc new file mode 100644 index 0000000000000..a44afa4474a7b --- /dev/null +++ b/docs/management/upgrade-assistant.asciidoc @@ -0,0 +1,29 @@ +[[upgrade-assistant]] +== Upgrade Assistant + +The Upgrade Assistant helps you prepare for your upgrade +to the next version of the {stack}. +To access the assistant, go to *{stack-manage-app} > Upgrade Assistant*. + +The assistant identifies deprecated settings in your configuration +and guides you through the process of resolving issues if any deprecated features are enabled. + +[discrete] +=== Required permissions + +The `manage` cluster privilege is required to access the *Upgrade assistant*. +Additional privileges may be needed to perform certain actions. + +[discrete] +=== Feature set +Some features of the Upgrade assistant are only needed when upgrading to a new major version. The feature set enabled by default are those for the very next version from the one Kibana currently runs on. + +[discrete] +=== Deprecations +The Upgrade assistant pulls information about deprecations from the following sources: + +* Elasticsearch Deprecation Info API +* Elasticsearch deprecation logs +* Kibana deprecations API + +For more information about the API's the Upgraed assistant provides, refer to <>. diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index a0193baaa0ab0..02f57f5789d77 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -396,11 +396,6 @@ This content has moved. Refer to <>. This content has moved. Refer to {stack-ref}/upgrading-kibana.html[Upgrade Kibana]. -[role="exclude",id="upgrade-assistant"] -== Upgrade Assistant - -This content has moved. Refer to {kibana-ref-all}/7.17/upgrade-assistant.html[Upgrade Assistant]. - [role="exclude",id="brew"] == Install {kib} on macOS with Homebrew @@ -424,4 +419,4 @@ This page has been deleted. Refer to <>. [role="exclude",id="managing-alerts-and-actions"] == Alerts and Actions -This page has been deleted. Refer to <>. \ No newline at end of file +This page has been deleted. Refer to <>. diff --git a/docs/setup/upgrade/resolving-migration-failures.asciidoc b/docs/setup/upgrade/resolving-migration-failures.asciidoc index 6de91d9a58db5..63264cb4f0963 100644 --- a/docs/setup/upgrade/resolving-migration-failures.asciidoc +++ b/docs/setup/upgrade/resolving-migration-failures.asciidoc @@ -133,7 +133,7 @@ Migrations will fail if saved objects belong to an unknown saved object type. Unknown saved objects are typically caused by performing manual modifications to the {es} index (no longer allowed in 8.x), or by disabling a plugin that had previously created a saved object. -We recommend using the {kibana-ref-all}/7.17/upgrade-assistant.html[Upgrade Assistant] +We recommend using the <> to discover and remedy any unknown saved object types. {kib} version 7.17.0 deployments containing unknown saved object types will also log the following warning message: diff --git a/docs/setup/upgrade/saved-objects-migration.asciidoc b/docs/setup/upgrade/saved-objects-migration.asciidoc index 5d84ece1c3c9f..3dac6a02c96ef 100644 --- a/docs/setup/upgrade/saved-objects-migration.asciidoc +++ b/docs/setup/upgrade/saved-objects-migration.asciidoc @@ -3,7 +3,7 @@ Each time you upgrade {kib}, an upgrade migration is performed to ensure that all <> are compatible with the new version. -NOTE: To help you prepare for the upgrade to 8.0.0, 7.17.0 includes an https://www.elastic.co/guide/en/kibana/7.17/upgrade-assistant.html[*Upgrade Assistant*]. +NOTE: {kib} includes an <> to help you prepare for an upgrade. To access the assistant, go to *Stack Management > Upgrade Assistant*. WARNING: {kib} 7.12.0 and later uses a new migration process and index naming scheme. Before you upgrade, read the documentation for your version of {kib}. diff --git a/docs/setup/upgrade/upgrade-standard.asciidoc b/docs/setup/upgrade/upgrade-standard.asciidoc index 6854ead7531a8..f78a26e8acc89 100644 --- a/docs/setup/upgrade/upgrade-standard.asciidoc +++ b/docs/setup/upgrade/upgrade-standard.asciidoc @@ -1,8 +1,7 @@ [[upgrade-standard]] === Standard upgrade -NOTE: 7.17 includes an https://www.elastic.co/guide/en/kibana/7.17/upgrade-assistant.html[Upgrade Assistant] -to help you prepare for your upgrade to 8.0. To access the assistant, go to *Stack Management > Upgrade Assistant*. +NOTE: {kib} includes an <> to help you prepare for an upgrade. To access the assistant, go to *{stack-manage-app} > Upgrade Assistant*. [IMPORTANT] =========================================== diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 67ccadf696411..5c78d91183861 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -200,6 +200,8 @@ include::{kib-repo-dir}/management/advanced-options.asciidoc[] include::{kib-repo-dir}/management/managing-tags.asciidoc[] +include::{kib-repo-dir}/management/upgrade-assistant.asciidoc[] + include::{kib-repo-dir}/management/watcher-ui/index.asciidoc[] From 40202c0be5fc2ec23d3f72c1ad118ef69ebf5d6f Mon Sep 17 00:00:00 2001 From: Antonio Date: Mon, 19 Dec 2022 17:26:52 +0100 Subject: [PATCH 16/55] [ResponseOps] [Cases] Update mapping for the severity field in cases (#147498) Connected to #132041 ## Summary Second PR migrating the Cases' Saved objects to enable sorting by additional fields in the all-cases view. - In this PR the case the severity field becomes a `short`. - Added data migrations for 8.7.0 converting the severity field values from `keyword` to `short`. - Added tests for the migrations. - Updated existing transforms to take severity into account. ES model to external model and vice versa. - Updated existing tests to take into account the new internal value type of the severity field. - Added a test for sorting by severity(although the functionality is not yet externally available). --- .../migrations/check_registered_types.test.ts | 2 +- .../plugins/cases/server/client/utils.test.ts | 10 +- x-pack/plugins/cases/server/client/utils.ts | 10 +- .../plugins/cases/server/common/constants.ts | 16 + .../cases/server/saved_object_types/cases.ts | 2 +- .../migrations/cases.test.ts | 47 +- .../saved_object_types/migrations/cases.ts | 15 + .../cases/server/services/cases/index.test.ts | 481 +++++++++++++----- .../cases/server/services/cases/index.ts | 1 + .../server/services/cases/transform.test.ts | 57 ++- .../cases/server/services/cases/transform.ts | 15 +- .../cases/server/services/cases/types.ts | 18 +- .../cases/server/services/test_utils.ts | 39 +- .../tests/common/cases/find_cases.ts | 29 ++ .../tests/common/cases/import_export.ts | 3 +- .../tests/common/cases/migrations.ts | 35 ++ .../cases/8.5.0/cases_severity.json | 191 +++++++ 17 files changed, 823 insertions(+), 148 deletions(-) create mode 100644 x-pack/test/functional/fixtures/kbn_archiver/cases/8.5.0/cases_severity.json diff --git a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts index 65670d5d21ff0..de9981618a01b 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts @@ -69,7 +69,7 @@ describe('checking migration metadata changes on all registered SO types', () => "canvas-element": "e2e312fc499c1a81e628b88baba492fb24f4e82d", "canvas-workpad": "4b05f7829bc805bbaa07eb9fc0d2a2bbbd6bbf39", "canvas-workpad-template": "d4bb65aa9c4a2b25029d3272fd9c715d8e4247d7", - "cases": "17af08c8b3550b3e57ba1a7f3b89d85f271712c0", + "cases": "dc9b349d343dab9c0fecac4104f9f6d4d068bcb2", "cases-comments": "d7c4c1d24e97620cd415e27e5eb7d5b5f2c5b461", "cases-configure": "1afc414f5563a36e4612fa269193d3ed7277c7bd", "cases-connector-mappings": "4b16d440af966e5d6e0fa33368bfa15d987a4b69", diff --git a/x-pack/plugins/cases/server/client/utils.test.ts b/x-pack/plugins/cases/server/client/utils.test.ts index 42989ca09aa8a..2ca1eac2c85d3 100644 --- a/x-pack/plugins/cases/server/client/utils.test.ts +++ b/x-pack/plugins/cases/server/client/utils.test.ts @@ -15,6 +15,7 @@ import { import { toElasticsearchQuery } from '@kbn/es-query'; import { CaseStatuses } from '../../common'; import { CaseSeverity } from '../../common/api'; +import { SEVERITY_EXTERNAL_TO_ESMODEL } from '../common/constants'; describe('utils', () => { describe('convertSortField', () => { @@ -407,8 +408,8 @@ describe('utils', () => { }); it('creates a filter for the severity', () => { - expect(constructQueryOptions({ severity: CaseSeverity.CRITICAL }).filter) - .toMatchInlineSnapshot(` + Object.values(CaseSeverity).forEach((severity) => { + expect(constructQueryOptions({ severity }).filter).toMatchInlineSnapshot(` Object { "arguments": Array [ Object { @@ -419,13 +420,14 @@ describe('utils', () => { Object { "isQuoted": false, "type": "literal", - "value": "critical", + "value": "${SEVERITY_EXTERNAL_TO_ESMODEL[severity]}", }, ], "function": "is", "type": "function", } - `); + `); + }); }); it('creates a filter for the time range', () => { diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index 1851750d862d9..88724a6a761d1 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -19,6 +19,8 @@ import { isCommentRequestTypePersistableState, } from '../../common/utils/attachments'; import { CASE_SAVED_OBJECT, NO_ASSIGNEES_FILTERING_KEYWORD } from '../../common/constants'; + +import { SEVERITY_EXTERNAL_TO_ESMODEL } from '../common/constants'; import type { CaseStatuses, CommentRequest, @@ -164,7 +166,10 @@ export const addSeverityFilter = ({ type?: string; }): KueryNode => { const filters: KueryNode[] = []; - filters.push(nodeBuilder.is(`${type}.attributes.severity`, severity)); + + filters.push( + nodeBuilder.is(`${type}.attributes.severity`, `${SEVERITY_EXTERNAL_TO_ESMODEL[severity]}`) + ); if (appendFilter) { filters.push(appendFilter); @@ -495,6 +500,7 @@ enum SortFieldCase { createdAt = 'created_at', status = 'status', title = 'title.keyword', + severity = 'severity', } export const convertSortField = (sortField: string | undefined): SortFieldCase => { @@ -509,6 +515,8 @@ export const convertSortField = (sortField: string | undefined): SortFieldCase = return SortFieldCase.closedAt; case 'title': return SortFieldCase.title; + case 'severity': + return SortFieldCase.severity; default: return SortFieldCase.createdAt; } diff --git a/x-pack/plugins/cases/server/common/constants.ts b/x-pack/plugins/cases/server/common/constants.ts index 632125bf29b14..7d64bea79b005 100644 --- a/x-pack/plugins/cases/server/common/constants.ts +++ b/x-pack/plugins/cases/server/common/constants.ts @@ -5,7 +5,9 @@ * 2.0. */ +import { CaseSeverity } from '../../common/api'; import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../common/constants'; +import { ESCaseSeverity } from '../services/cases/types'; /** * The name of the saved object reference indicating the action connector ID. This is stored in the Saved Object reference @@ -37,3 +39,17 @@ export const EXTERNAL_REFERENCE_REF_NAME = 'externalReferenceId'; * The name of the licensing feature to notify for feature usage with the licensing plugin */ export const LICENSING_CASE_ASSIGNMENT_FEATURE = 'Cases user assignment'; + +export const SEVERITY_EXTERNAL_TO_ESMODEL: Record = { + [CaseSeverity.LOW]: ESCaseSeverity.LOW, + [CaseSeverity.MEDIUM]: ESCaseSeverity.MEDIUM, + [CaseSeverity.HIGH]: ESCaseSeverity.HIGH, + [CaseSeverity.CRITICAL]: ESCaseSeverity.CRITICAL, +}; + +export const SEVERITY_ESMODEL_TO_EXTERNAL: Record = { + [ESCaseSeverity.LOW]: CaseSeverity.LOW, + [ESCaseSeverity.MEDIUM]: CaseSeverity.MEDIUM, + [ESCaseSeverity.HIGH]: CaseSeverity.HIGH, + [ESCaseSeverity.CRITICAL]: CaseSeverity.CRITICAL, +}; diff --git a/x-pack/plugins/cases/server/saved_object_types/cases.ts b/x-pack/plugins/cases/server/saved_object_types/cases.ts index b34a355dfcd3e..50c1e4ccad92d 100644 --- a/x-pack/plugins/cases/server/saved_object_types/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/cases.ts @@ -177,7 +177,7 @@ export const createCaseSavedObjectType = ( }, }, severity: { - type: 'keyword', + type: 'short', }, }, }, diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts index c23bbd29dfe99..90c3724c16af3 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts @@ -5,11 +5,13 @@ * 2.0. */ -import type { SavedObjectSanitizedDoc } from '@kbn/core/server'; +import type { SavedObjectSanitizedDoc, SavedObjectUnsanitizedDoc } from '@kbn/core/server'; import type { CaseAttributes, CaseFullExternalService } from '../../../common/api'; import { CaseSeverity, ConnectorTypes, NONE_CONNECTOR_ID } from '../../../common/api'; import { CASE_SAVED_OBJECT } from '../../../common/constants'; +import { SEVERITY_EXTERNAL_TO_ESMODEL } from '../../common/constants'; import { getNoneCaseConnector } from '../../common/utils'; +import { ESCaseSeverity } from '../../services/cases/types'; import type { ESCaseConnectorWithId } from '../../services/test_utils'; import { createExternalService } from '../../services/test_utils'; import { @@ -17,6 +19,7 @@ import { addDuration, addSeverity, caseConnectorIdMigration, + convertSeverity, removeCaseType, } from './cases'; @@ -577,4 +580,46 @@ describe('case migrations', () => { }); }); }); + + describe('update severity', () => { + for (const oldSeverityValue of Object.values(CaseSeverity)) { + it(`migrates ${oldSeverityValue} severity string label to matching number`, () => { + const doc = { + id: '123', + type: 'abc', + attributes: { + severity: oldSeverityValue, + }, + references: [], + } as unknown as SavedObjectUnsanitizedDoc; + + expect(convertSeverity(doc)).toEqual({ + ...doc, + attributes: { + ...doc.attributes, + severity: SEVERITY_EXTERNAL_TO_ESMODEL[oldSeverityValue], + }, + references: [], + }); + }); + } + + it('default value for severity is 0 if it does not exist', () => { + const doc = { + id: '123', + type: 'abc', + attributes: {}, + references: [], + } as unknown as SavedObjectUnsanitizedDoc; + + expect(convertSeverity(doc)).toEqual({ + ...doc, + attributes: { + ...doc.attributes, + severity: ESCaseSeverity.LOW, + }, + references: [], + }); + }); + }); }); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts index 50ea3e1dd8064..593fb9bdada1b 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts @@ -14,9 +14,11 @@ import { addOwnerToSO } from '.'; import type { ESConnectorFields } from '../../services'; import type { CaseAttributes } from '../../../common/api'; import { CaseSeverity, ConnectorTypes } from '../../../common/api'; + import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME, + SEVERITY_EXTERNAL_TO_ESMODEL, } from '../../common/constants'; import { transformConnectorIdToReference, @@ -24,6 +26,7 @@ import { } from './user_actions/connector_id'; import { CASE_TYPE_INDIVIDUAL } from './constants'; import { pipeMigrations } from './utils'; +import { ESCaseSeverity } from '../../services/cases/types'; interface UnsanitizedCaseConnector { connector_id: string; @@ -131,6 +134,17 @@ export const addAssignees = ( return { ...doc, attributes: { ...doc.attributes, assignees }, references: doc.references ?? [] }; }; +export const convertSeverity = ( + doc: SavedObjectUnsanitizedDoc +): SavedObjectSanitizedDoc & { severity: ESCaseSeverity }> => { + const severity = SEVERITY_EXTERNAL_TO_ESMODEL[doc.attributes.severity] ?? ESCaseSeverity.LOW; + return { + ...doc, + attributes: { ...doc.attributes, severity }, + references: doc.references ?? [], + }; +}; + export const caseMigrations = { '7.10.0': ( doc: SavedObjectUnsanitizedDoc @@ -194,4 +208,5 @@ export const caseMigrations = { '8.1.0': removeCaseType, '8.3.0': pipeMigrations(addDuration, addSeverity), '8.5.0': addAssignees, + '8.7.0': convertSeverity, }; diff --git a/x-pack/plugins/cases/server/services/cases/index.test.ts b/x-pack/plugins/cases/server/services/cases/index.test.ts index 71f2c96cc2cbc..0c1251cf373df 100644 --- a/x-pack/plugins/cases/server/services/cases/index.test.ts +++ b/x-pack/plugins/cases/server/services/cases/index.test.ts @@ -14,6 +14,7 @@ */ import type { CaseAttributes, CaseConnector, CaseFullExternalService } from '../../../common/api'; +import { CaseSeverity } from '../../../common/api'; import { CASE_SAVED_OBJECT } from '../../../common/constants'; import { savedObjectsClientMock } from '@kbn/core/server/mocks'; import type { @@ -41,6 +42,7 @@ import { createSOFindResponse, } from '../test_utils'; import type { ESCaseAttributes } from './types'; +import { ESCaseSeverity } from './types'; import { AttachmentService } from '../attachments'; import { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry'; import type { CaseSavedObject } from '../../common/types'; @@ -48,9 +50,11 @@ import type { CaseSavedObject } from '../../common/types'; const createUpdateSOResponse = ({ connector, externalService, + severity, }: { connector?: ESCaseConnectorWithId; externalService?: CaseFullExternalService; + severity?: ESCaseSeverity; } = {}): SavedObjectsUpdateResponse => { const references: SavedObjectReference[] = createSavedObjectReferences({ connector, @@ -71,6 +75,10 @@ const createUpdateSOResponse = ({ attributes = { ...attributes, external_service: null }; } + if (severity || severity === 0) { + attributes = { ...attributes, severity }; + } + return { type: CASE_SAVED_OBJECT, id: '1', @@ -83,6 +91,7 @@ const createFindSO = ( params: { connector?: ESCaseConnectorWithId; externalService?: CaseFullExternalService; + overrides?: Partial; } = {} ): SavedObjectsFindResult => ({ ...createCaseSavedObjectResponse(params), @@ -91,18 +100,22 @@ const createFindSO = ( const createCaseUpdateParams = ( connector?: CaseConnector, - externalService?: CaseFullExternalService + externalService?: CaseFullExternalService, + severity?: CaseSeverity ): Partial => ({ ...(connector && { connector }), ...(externalService && { external_service: externalService }), + ...(severity && { severity }), }); const createCasePostParams = ( connector: CaseConnector, - externalService?: CaseFullExternalService + externalService?: CaseFullExternalService, + severity?: CaseSeverity ): CaseAttributes => ({ ...basicCaseFields, connector, + ...(severity ? { severity } : { severity: basicCaseFields.severity }), ...(externalService ? { external_service: externalService } : { external_service: null }), }); @@ -141,13 +154,17 @@ describe('CasesService', () => { describe('transforms the external model to the Elasticsearch model', () => { describe('patch', () => { it('includes the passed in fields', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) + unsecuredSavedObjectsClient.update.mockResolvedValue( + {} as SavedObjectsUpdateResponse ); await service.patchCase({ caseId: '1', - updatedAttributes: createCasePostParams(createJiraConnector(), createExternalService()), + updatedAttributes: createCasePostParams( + createJiraConnector(), + createExternalService(), + CaseSeverity.CRITICAL + ), originalCase: {} as CaseSavedObject, }); @@ -173,7 +190,7 @@ describe('CasesService', () => { "settings": Object { "syncAlerts": true, }, - "severity": "low", + "severity": 30, "status": "open", "tags": Array [ "defacement", @@ -190,8 +207,8 @@ describe('CasesService', () => { }); it('transforms the connector.fields to an array of key/value pairs', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) + unsecuredSavedObjectsClient.update.mockResolvedValue( + {} as SavedObjectsUpdateResponse ); await service.patchCase({ @@ -221,8 +238,8 @@ describe('CasesService', () => { }); it('preserves the connector fields but does not have the id', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) + unsecuredSavedObjectsClient.update.mockResolvedValue( + {} as SavedObjectsUpdateResponse ); await service.patchCase({ @@ -256,8 +273,8 @@ describe('CasesService', () => { }); it('removes the connector id and adds it to the references', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) + unsecuredSavedObjectsClient.update.mockResolvedValue( + {} as SavedObjectsUpdateResponse ); await service.patchCase({ @@ -284,8 +301,8 @@ describe('CasesService', () => { }); it('removes the external_service connector_id and adds it to the references', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) + unsecuredSavedObjectsClient.update.mockResolvedValue( + {} as SavedObjectsUpdateResponse ); await service.patchCase({ @@ -312,8 +329,8 @@ describe('CasesService', () => { }); it('builds references for external service connector id, case connector id, and includes the existing references', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) + unsecuredSavedObjectsClient.update.mockResolvedValue( + {} as SavedObjectsUpdateResponse ); await service.patchCase({ @@ -348,8 +365,8 @@ describe('CasesService', () => { }); it('builds references for connector_id and preserves the existing connector.id reference', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) + unsecuredSavedObjectsClient.update.mockResolvedValue( + {} as SavedObjectsUpdateResponse ); await service.patchCase({ @@ -381,8 +398,8 @@ describe('CasesService', () => { }); it('preserves the external_service fields except for the connector_id', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) + unsecuredSavedObjectsClient.update.mockResolvedValue( + {} as SavedObjectsUpdateResponse ); await service.patchCase({ @@ -410,8 +427,8 @@ describe('CasesService', () => { }); it('creates an empty updatedAttributes when there is no connector or external_service as input', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) + unsecuredSavedObjectsClient.update.mockResolvedValue( + {} as SavedObjectsUpdateResponse ); await service.patchCase({ @@ -429,8 +446,8 @@ describe('CasesService', () => { }); it('creates a updatedAttributes field with the none connector', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) + unsecuredSavedObjectsClient.update.mockResolvedValue( + {} as SavedObjectsUpdateResponse ); await service.patchCase({ @@ -449,13 +466,98 @@ describe('CasesService', () => { } `); }); + + it.each([ + [CaseSeverity.LOW, ESCaseSeverity.LOW], + [CaseSeverity.MEDIUM, ESCaseSeverity.MEDIUM], + [CaseSeverity.HIGH, ESCaseSeverity.HIGH], + [CaseSeverity.CRITICAL, ESCaseSeverity.CRITICAL], + ])( + 'properly converts "%s" severity to corresponding ES value on updating SO', + async (patchParamsSeverity, expectedSeverity) => { + unsecuredSavedObjectsClient.update.mockResolvedValue( + {} as SavedObjectsUpdateResponse + ); + + await service.patchCase({ + caseId: '1', + updatedAttributes: createCaseUpdateParams(undefined, undefined, patchParamsSeverity), + originalCase: {} as CaseSavedObject, + }); + + const patchAttributes = unsecuredSavedObjectsClient.update.mock + .calls[0][2] as ESCaseAttributes; + + expect(patchAttributes.severity).toEqual(expectedSeverity); + } + ); + }); + + describe('bulkPatch', () => { + it('properly converts severity to corresponding ES value on bulk updating SO', async () => { + unsecuredSavedObjectsClient.bulkUpdate.mockResolvedValue({ + saved_objects: [ + createCaseSavedObjectResponse({ caseId: '1' }), + createCaseSavedObjectResponse({ caseId: '2' }), + createCaseSavedObjectResponse({ caseId: '3' }), + createCaseSavedObjectResponse({ caseId: '4' }), + ], + }); + + await service.patchCases({ + cases: [ + { + caseId: '1', + updatedAttributes: createCasePostParams( + getNoneCaseConnector(), + undefined, + CaseSeverity.LOW + ), + originalCase: {} as CaseSavedObject, + }, + { + caseId: '2', + updatedAttributes: createCasePostParams( + getNoneCaseConnector(), + undefined, + CaseSeverity.MEDIUM + ), + originalCase: {} as CaseSavedObject, + }, + { + caseId: '3', + updatedAttributes: createCasePostParams( + getNoneCaseConnector(), + undefined, + CaseSeverity.HIGH + ), + originalCase: {} as CaseSavedObject, + }, + { + caseId: '4', + updatedAttributes: createCasePostParams( + getNoneCaseConnector(), + undefined, + CaseSeverity.CRITICAL + ), + originalCase: {} as CaseSavedObject, + }, + ], + }); + + const patchResults = unsecuredSavedObjectsClient.bulkUpdate.mock + .calls[0][0] as unknown as Array>; + + expect(patchResults[0].attributes.severity).toEqual(ESCaseSeverity.LOW); + expect(patchResults[1].attributes.severity).toEqual(ESCaseSeverity.MEDIUM); + expect(patchResults[2].attributes.severity).toEqual(ESCaseSeverity.HIGH); + expect(patchResults[3].attributes.severity).toEqual(ESCaseSeverity.CRITICAL); + }); }); describe('post', () => { it('creates a null external_service field when the attribute was null in the creation parameters', async () => { - unsecuredSavedObjectsClient.create.mockReturnValue( - Promise.resolve({} as SavedObject) - ); + unsecuredSavedObjectsClient.create.mockResolvedValue({} as SavedObject); await service.postNewCase({ attributes: createCasePostParams(createJiraConnector()), @@ -468,9 +570,7 @@ describe('CasesService', () => { }); it('includes the creation attributes excluding the connector.id and connector_id', async () => { - unsecuredSavedObjectsClient.create.mockReturnValue( - Promise.resolve({} as SavedObject) - ); + unsecuredSavedObjectsClient.create.mockResolvedValue({} as SavedObject); await service.postNewCase({ attributes: createCasePostParams(createJiraConnector(), createExternalService()), @@ -528,7 +628,7 @@ describe('CasesService', () => { "settings": Object { "syncAlerts": true, }, - "severity": "low", + "severity": 0, "status": "open", "tags": Array [ "defacement", @@ -563,9 +663,7 @@ describe('CasesService', () => { }); it('moves the connector.id and connector_id to the references', async () => { - unsecuredSavedObjectsClient.create.mockReturnValue( - Promise.resolve({} as SavedObject) - ); + unsecuredSavedObjectsClient.create.mockResolvedValue({} as SavedObject); await service.postNewCase({ attributes: createCasePostParams(createJiraConnector(), createExternalService()), @@ -591,9 +689,7 @@ describe('CasesService', () => { }); it('sets fields to an empty array when it is not included with the connector', async () => { - unsecuredSavedObjectsClient.create.mockReturnValue( - Promise.resolve({} as SavedObject) - ); + unsecuredSavedObjectsClient.create.mockResolvedValue({} as SavedObject); await service.postNewCase({ attributes: createCasePostParams( @@ -609,9 +705,7 @@ describe('CasesService', () => { }); it('does not create a reference for a none connector', async () => { - unsecuredSavedObjectsClient.create.mockReturnValue( - Promise.resolve({} as SavedObject) - ); + unsecuredSavedObjectsClient.create.mockResolvedValue({} as SavedObject); await service.postNewCase({ attributes: createCasePostParams(getNoneCaseConnector()), @@ -624,9 +718,7 @@ describe('CasesService', () => { }); it('does not create a reference for an external_service field that is null', async () => { - unsecuredSavedObjectsClient.create.mockReturnValue( - Promise.resolve({} as SavedObject) - ); + unsecuredSavedObjectsClient.create.mockResolvedValue({} as SavedObject); await service.postNewCase({ attributes: createCasePostParams(getNoneCaseConnector()), @@ -637,26 +729,45 @@ describe('CasesService', () => { .calls[0][2] as SavedObjectsCreateOptions; expect(creationOptions.references).toEqual([]); }); + + it.each([ + [CaseSeverity.LOW, ESCaseSeverity.LOW], + [CaseSeverity.MEDIUM, ESCaseSeverity.MEDIUM], + [CaseSeverity.HIGH, ESCaseSeverity.HIGH], + [CaseSeverity.CRITICAL, ESCaseSeverity.CRITICAL], + ])( + 'properly converts "%s" severity to corresponding ES value on creating SO', + async (postParamsSeverity, expectedSeverity) => { + unsecuredSavedObjectsClient.create.mockResolvedValue({} as SavedObject); + + await service.postNewCase({ + attributes: createCasePostParams(getNoneCaseConnector(), undefined, postParamsSeverity), + id: '1', + }); + + const postAttributes = unsecuredSavedObjectsClient.create.mock + .calls[0][1] as ESCaseAttributes; + expect(postAttributes.severity).toEqual(expectedSeverity); + } + ); }); }); describe('transforms the Elasticsearch model to the external model', () => { describe('bulkPatch', () => { it('formats the update saved object by including the passed in fields and transforming the connector.fields', async () => { - unsecuredSavedObjectsClient.bulkUpdate.mockReturnValue( - Promise.resolve({ - saved_objects: [ - createCaseSavedObjectResponse({ - connector: createESJiraConnector(), - externalService: createExternalService(), - }), - createCaseSavedObjectResponse({ - connector: createESJiraConnector({ id: '2' }), - externalService: createExternalService({ connector_id: '200' }), - }), - ], - }) - ); + unsecuredSavedObjectsClient.bulkUpdate.mockResolvedValue({ + saved_objects: [ + createCaseSavedObjectResponse({ + connector: createESJiraConnector(), + externalService: createExternalService(), + }), + createCaseSavedObjectResponse({ + connector: createESJiraConnector({ id: '2' }), + externalService: createExternalService({ connector_id: '200' }), + }), + ], + }); const res = await service.patchCases({ cases: [ @@ -703,12 +814,37 @@ describe('CasesService', () => { res.saved_objects[0].attributes.external_service?.connector_id ).toMatchInlineSnapshot(`"100"`); }); + + it('properly converts the severity field to the corresponding external value the bulkPatch response', async () => { + unsecuredSavedObjectsClient.bulkUpdate.mockResolvedValue({ + saved_objects: [ + createCaseSavedObjectResponse({ overrides: { severity: ESCaseSeverity.LOW } }), + createCaseSavedObjectResponse({ overrides: { severity: ESCaseSeverity.MEDIUM } }), + createCaseSavedObjectResponse({ overrides: { severity: ESCaseSeverity.HIGH } }), + createCaseSavedObjectResponse({ overrides: { severity: ESCaseSeverity.CRITICAL } }), + ], + }); + + const res = await service.patchCases({ + cases: [ + { + caseId: '1', + updatedAttributes: createCasePostParams(getNoneCaseConnector()), + originalCase: {} as CaseSavedObject, + }, + ], + }); + expect(res.saved_objects[0].attributes.severity).toEqual(CaseSeverity.LOW); + expect(res.saved_objects[1].attributes.severity).toEqual(CaseSeverity.MEDIUM); + expect(res.saved_objects[2].attributes.severity).toEqual(CaseSeverity.HIGH); + expect(res.saved_objects[3].attributes.severity).toEqual(CaseSeverity.CRITICAL); + }); }); describe('patch', () => { it('returns an object with a none connector and without a reference when it was set to a none connector in the update', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve(createUpdateSOResponse({ connector: getNoneCaseConnector() })) + unsecuredSavedObjectsClient.update.mockResolvedValue( + createUpdateSOResponse({ connector: getNoneCaseConnector() }) ); const res = await service.patchCase({ @@ -731,8 +867,8 @@ describe('CasesService', () => { }); it('returns an object with a null external service and without a reference when it was set to null in the update', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve(createUpdateSOResponse({ externalService: null })) + unsecuredSavedObjectsClient.update.mockResolvedValue( + createUpdateSOResponse({ externalService: null }) ); const res = await service.patchCase({ @@ -750,9 +886,7 @@ describe('CasesService', () => { }); it('returns an empty object when neither the connector or external service was updated', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve(createUpdateSOResponse()) - ); + unsecuredSavedObjectsClient.update.mockResolvedValue(createUpdateSOResponse()); const res = await service.patchCase({ caseId: '1', @@ -765,8 +899,8 @@ describe('CasesService', () => { }); it('returns an undefined connector if it is not returned by the update', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) + unsecuredSavedObjectsClient.update.mockResolvedValue( + {} as SavedObjectsUpdateResponse ); const res = await service.patchCase({ @@ -798,7 +932,7 @@ describe('CasesService', () => { references: undefined, }; - unsecuredSavedObjectsClient.update.mockReturnValue(Promise.resolve(returnValue)); + unsecuredSavedObjectsClient.update.mockResolvedValue(returnValue); const res = await service.patchCase({ caseId: '1', @@ -828,7 +962,7 @@ describe('CasesService', () => { references: undefined, }; - unsecuredSavedObjectsClient.update.mockReturnValue(Promise.resolve(returnValue)); + unsecuredSavedObjectsClient.update.mockResolvedValue(returnValue); const res = await service.patchCase({ caseId: '1', @@ -851,7 +985,7 @@ describe('CasesService', () => { references: undefined, }; - unsecuredSavedObjectsClient.update.mockReturnValue(Promise.resolve(returnValue)); + unsecuredSavedObjectsClient.update.mockResolvedValue(returnValue); const res = await service.patchCase({ caseId: '1', @@ -885,8 +1019,8 @@ describe('CasesService', () => { }); it('returns the connector.id after finding the reference', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve(createUpdateSOResponse({ connector: createESJiraConnector() })) + unsecuredSavedObjectsClient.update.mockResolvedValue( + createUpdateSOResponse({ connector: createESJiraConnector() }) ); const res = await service.patchCase({ @@ -911,8 +1045,8 @@ describe('CasesService', () => { }); it('returns the external_service connector_id after finding the reference', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve(createUpdateSOResponse({ externalService: createExternalService() })) + unsecuredSavedObjectsClient.update.mockResolvedValue( + createUpdateSOResponse({ externalService: createExternalService() }) ); const res = await service.patchCase({ @@ -938,17 +1072,37 @@ describe('CasesService', () => { `); expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"100"`); }); + + it.each([ + [ESCaseSeverity.LOW, CaseSeverity.LOW], + [ESCaseSeverity.MEDIUM, CaseSeverity.MEDIUM], + [ESCaseSeverity.HIGH, CaseSeverity.HIGH], + [ESCaseSeverity.CRITICAL, CaseSeverity.CRITICAL], + ])( + 'properly converts "%s" severity to corresponding external value in the patch response', + async (internalSeverityValue, expectedSeverity) => { + unsecuredSavedObjectsClient.update.mockResolvedValue( + createUpdateSOResponse({ severity: internalSeverityValue }) + ); + + const res = await service.patchCase({ + caseId: '1', + updatedAttributes: createCaseUpdateParams(), + originalCase: {} as CaseSavedObject, + }); + + expect(res.attributes.severity).toEqual(expectedSeverity); + } + ); }); describe('post', () => { it('includes the connector.id and connector_id fields in the response', async () => { - unsecuredSavedObjectsClient.create.mockReturnValue( - Promise.resolve( - createCaseSavedObjectResponse({ - connector: createESJiraConnector(), - externalService: createExternalService(), - }) - ) + unsecuredSavedObjectsClient.create.mockResolvedValue( + createCaseSavedObjectResponse({ + connector: createESJiraConnector(), + externalService: createExternalService(), + }) ); const res = await service.postNewCase({ @@ -959,6 +1113,27 @@ describe('CasesService', () => { expect(res.attributes.connector.id).toMatchInlineSnapshot(`"1"`); expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"100"`); }); + + it.each([ + [ESCaseSeverity.LOW, CaseSeverity.LOW], + [ESCaseSeverity.MEDIUM, CaseSeverity.MEDIUM], + [ESCaseSeverity.HIGH, CaseSeverity.HIGH], + [ESCaseSeverity.CRITICAL, CaseSeverity.CRITICAL], + ])( + 'properly converts "%s" severity to corresponding external value in the post response', + async (internalSeverityValue, expectedSeverity) => { + unsecuredSavedObjectsClient.create.mockResolvedValue( + createCaseSavedObjectResponse({ overrides: { severity: internalSeverityValue } }) + ); + + const res = await service.postNewCase({ + attributes: createCasePostParams(getNoneCaseConnector()), + id: '1', + }); + + expect(res.attributes.severity).toEqual(expectedSeverity); + } + ); }); describe('find', () => { @@ -970,7 +1145,7 @@ describe('CasesService', () => { }), createFindSO(), ]); - unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn)); + unsecuredSavedObjectsClient.find.mockResolvedValue(findMockReturn); const res = await service.findCases(); expect(res.saved_objects[0].attributes.connector.id).toMatchInlineSnapshot(`"1"`); @@ -987,7 +1162,7 @@ describe('CasesService', () => { }), createFindSO(), ]); - unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn)); + unsecuredSavedObjectsClient.find.mockResolvedValue(findMockReturn); const res = await service.findCases(); const { saved_objects: ignored, ...findResponseFields } = res; @@ -999,24 +1174,41 @@ describe('CasesService', () => { } `); }); + + it.each([ + [ESCaseSeverity.LOW, CaseSeverity.LOW], + [ESCaseSeverity.MEDIUM, CaseSeverity.MEDIUM], + [ESCaseSeverity.HIGH, CaseSeverity.HIGH], + [ESCaseSeverity.CRITICAL, CaseSeverity.CRITICAL], + ])( + 'includes the properly converted "%s" severity field in the result', + async (severity, expectedSeverity) => { + const findMockReturn = createSOFindResponse([ + createFindSO({ overrides: { severity } }), + createFindSO(), + ]); + unsecuredSavedObjectsClient.find.mockResolvedValue(findMockReturn); + + const res = await service.findCases(); + expect(res.saved_objects[0].attributes.severity).toEqual(expectedSeverity); + } + ); }); describe('bulkGet', () => { it('includes the connector.id and connector_id fields in the response', async () => { - unsecuredSavedObjectsClient.bulkGet.mockReturnValue( - Promise.resolve({ - saved_objects: [ - createCaseSavedObjectResponse({ - connector: createESJiraConnector(), - externalService: createExternalService(), - }), - createCaseSavedObjectResponse({ - connector: createESJiraConnector({ id: '2' }), - externalService: createExternalService({ connector_id: '200' }), - }), - ], - }) - ); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValue({ + saved_objects: [ + createCaseSavedObjectResponse({ + connector: createESJiraConnector(), + externalService: createExternalService(), + }), + createCaseSavedObjectResponse({ + connector: createESJiraConnector({ id: '2' }), + externalService: createExternalService({ connector_id: '200' }), + }), + ], + }); const res = await service.getCases({ caseIds: ['a'] }); @@ -1030,17 +1222,40 @@ describe('CasesService', () => { res.saved_objects[1].attributes.external_service?.connector_id ).toMatchInlineSnapshot(`"200"`); }); + + it('includes all severity values properly converted in the response', async () => { + unsecuredSavedObjectsClient.bulkGet.mockResolvedValue({ + saved_objects: [ + createCaseSavedObjectResponse({ + overrides: { severity: ESCaseSeverity.LOW }, + }), + createCaseSavedObjectResponse({ + overrides: { severity: ESCaseSeverity.MEDIUM }, + }), + createCaseSavedObjectResponse({ + overrides: { severity: ESCaseSeverity.HIGH }, + }), + createCaseSavedObjectResponse({ + overrides: { severity: ESCaseSeverity.CRITICAL }, + }), + ], + }); + + const res = await service.getCases({ caseIds: ['a'] }); + expect(res.saved_objects[0].attributes.severity).toEqual(CaseSeverity.LOW); + expect(res.saved_objects[1].attributes.severity).toEqual(CaseSeverity.MEDIUM); + expect(res.saved_objects[2].attributes.severity).toEqual(CaseSeverity.HIGH); + expect(res.saved_objects[3].attributes.severity).toEqual(CaseSeverity.CRITICAL); + }); }); describe('get', () => { it('includes the connector.id and connector_id fields in the response', async () => { - unsecuredSavedObjectsClient.get.mockReturnValue( - Promise.resolve( - createCaseSavedObjectResponse({ - connector: createESJiraConnector(), - externalService: createExternalService(), - }) - ) + unsecuredSavedObjectsClient.get.mockResolvedValue( + createCaseSavedObjectResponse({ + connector: createESJiraConnector(), + externalService: createExternalService(), + }) ); const res = await service.getCase({ id: 'a' }); @@ -1050,10 +1265,8 @@ describe('CasesService', () => { }); it('defaults to the none connector when the connector reference cannot be found', async () => { - unsecuredSavedObjectsClient.get.mockReturnValue( - Promise.resolve( - createCaseSavedObjectResponse({ externalService: createExternalService() }) - ) + unsecuredSavedObjectsClient.get.mockResolvedValue( + createCaseSavedObjectResponse({ externalService: createExternalService() }) ); const res = await service.getCase({ id: 'a' }); @@ -1068,18 +1281,14 @@ describe('CasesService', () => { }); it('sets external services connector_id to null when the connector id cannot be found in the references', async () => { - unsecuredSavedObjectsClient.get.mockReturnValue( - Promise.resolve(createCaseSavedObjectResponse()) - ); + unsecuredSavedObjectsClient.get.mockResolvedValue(createCaseSavedObjectResponse()); const res = await service.getCase({ id: 'a' }); expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"none"`); }); it('includes the external services fields when the connector id cannot be found in the references', async () => { - unsecuredSavedObjectsClient.get.mockReturnValue( - Promise.resolve(createCaseSavedObjectResponse()) - ); + unsecuredSavedObjectsClient.get.mockResolvedValue(createCaseSavedObjectResponse()); const res = await service.getCase({ id: 'a' }); expect(res.attributes.external_service).toMatchInlineSnapshot(` @@ -1100,17 +1309,15 @@ describe('CasesService', () => { }); it('defaults to the none connector and null external_services when attributes is undefined', async () => { - unsecuredSavedObjectsClient.get.mockReturnValue( - Promise.resolve({ - references: [ - { - id: '1', - name: CONNECTOR_ID_REFERENCE_NAME, - type: ACTION_SAVED_OBJECT_TYPE, - }, - ], - } as unknown as SavedObject) - ); + unsecuredSavedObjectsClient.get.mockResolvedValue({ + references: [ + { + id: '1', + name: CONNECTOR_ID_REFERENCE_NAME, + type: ACTION_SAVED_OBJECT_TYPE, + }, + ], + } as unknown as SavedObject); const res = await service.getCase({ id: 'a' }); expect(res.attributes.connector).toMatchInlineSnapshot(` @@ -1126,11 +1333,9 @@ describe('CasesService', () => { }); it('returns a null external_services when it is already null', async () => { - unsecuredSavedObjectsClient.get.mockReturnValue( - Promise.resolve({ - attributes: { external_service: null }, - } as SavedObject) - ); + unsecuredSavedObjectsClient.get.mockResolvedValue({ + attributes: { external_service: null }, + } as SavedObject); const res = await service.getCase({ id: 'a' }); expect(res.attributes.connector).toMatchInlineSnapshot(` @@ -1144,6 +1349,24 @@ describe('CasesService', () => { expect(res.attributes.external_service).toMatchInlineSnapshot(`null`); }); + + it.each([ + [ESCaseSeverity.LOW, CaseSeverity.LOW], + [ESCaseSeverity.MEDIUM, CaseSeverity.MEDIUM], + [ESCaseSeverity.HIGH, CaseSeverity.HIGH], + [ESCaseSeverity.CRITICAL, CaseSeverity.CRITICAL], + ])( + 'includes the properly converted "%s" severity field in the result', + async (internalSeverityValue, expectedSeverity) => { + unsecuredSavedObjectsClient.get.mockResolvedValue({ + attributes: { severity: internalSeverityValue }, + } as SavedObject); + + const res = await service.getCase({ id: 'a' }); + + expect(res.attributes.severity).toEqual(expectedSeverity); + } + ); }); }); diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 86d89ff4ef869..8a6abb2eba181 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -626,6 +626,7 @@ export class CasesService { bulkUpdate, { refresh } ); + return transformUpdateResponsesToExternalModels(updatedCases); } catch (error) { this.log.error(`Error on UPDATE case ${cases.map((c) => c.caseId).join(', ')}: ${error}`); diff --git a/x-pack/plugins/cases/server/services/cases/transform.test.ts b/x-pack/plugins/cases/server/services/cases/transform.test.ts index 93b417c5b73b4..f1a28381ab51d 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.test.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.test.ts @@ -17,12 +17,15 @@ import { transformUpdateResponseToExternalModel, } from './transform'; import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server'; -import { ConnectorTypes } from '../../../common/api'; +import { CaseSeverity, ConnectorTypes } from '../../../common/api'; import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME, + SEVERITY_ESMODEL_TO_EXTERNAL, + SEVERITY_EXTERNAL_TO_ESMODEL, } from '../../common/constants'; import { getNoneCaseConnector } from '../../common/utils'; +import { ESCaseSeverity } from './types'; describe('case transforms', () => { describe('transformUpdateResponseToExternalModel', () => { @@ -341,6 +344,28 @@ describe('case transforms', () => { expect(transformedAttributes.attributes.connector).not.toHaveProperty('id'); expect(transformedAttributes.referenceHandler.build()).toEqual([]); }); + + it.each([ + [CaseSeverity.LOW], + [CaseSeverity.MEDIUM], + [CaseSeverity.HIGH], + [CaseSeverity.CRITICAL], + ])('properly converts "%s" severity to corresponding ES Value', (externalSeverityValue) => { + const transformedAttributes = transformAttributesToESModel({ + severity: externalSeverityValue, + }); + + expect(transformedAttributes.attributes).toHaveProperty('severity'); + expect(transformedAttributes.attributes.severity).toBe( + SEVERITY_EXTERNAL_TO_ESMODEL[externalSeverityValue] + ); + }); + + it('does not return the severity when it is undefined', () => { + expect(transformAttributesToESModel({ severity: undefined }).attributes).not.toHaveProperty( + 'severity' + ); + }); }); describe('transformSavedObjectToExternalModel', () => { @@ -410,5 +435,35 @@ describe('case transforms', () => { } `); }); + + it.each([ + [ESCaseSeverity.LOW], + [ESCaseSeverity.MEDIUM], + [ESCaseSeverity.HIGH], + [ESCaseSeverity.CRITICAL], + ])( + 'properly converts "%s" severity to corresponding external value', + (internalSeverityValue) => { + const caseSO = createCaseSavedObjectResponse({ + overrides: { severity: internalSeverityValue }, + }); + + expect(caseSO.attributes).toHaveProperty('severity'); + expect(caseSO.attributes.severity).toBe(internalSeverityValue); + + const transformedSO = transformSavedObjectToExternalModel(caseSO); + + expect(transformedSO.attributes).toHaveProperty('severity'); + expect(transformedSO.attributes.severity).toBe( + SEVERITY_ESMODEL_TO_EXTERNAL[internalSeverityValue] + ); + } + ); + + it('does not return the severity when it is undefined', () => { + expect(transformAttributesToESModel({ severity: undefined }).attributes).not.toHaveProperty( + 'severity' + ); + }); }); }); diff --git a/x-pack/plugins/cases/server/services/cases/transform.ts b/x-pack/plugins/cases/server/services/cases/transform.ts index 1204375a0982e..de52bdbe57b04 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.ts @@ -20,9 +20,11 @@ import type { ESCaseAttributes, ExternalServicesWithoutConnectorId } from './typ import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME, + SEVERITY_ESMODEL_TO_EXTERNAL, + SEVERITY_EXTERNAL_TO_ESMODEL, } from '../../common/constants'; import type { CaseAttributes, CaseFullExternalService } from '../../../common/api'; -import { NONE_CONNECTOR_ID } from '../../../common/api'; +import { CaseSeverity, NONE_CONNECTOR_ID } from '../../../common/api'; import { findConnectorIdReference, transformFieldsToESModel, @@ -47,7 +49,8 @@ export function transformUpdateResponsesToExternalModels( export function transformUpdateResponseToExternalModel( updatedCase: SavedObjectsUpdateResponse ): SavedObjectsUpdateResponse { - const { connector, external_service, ...restUpdateAttributes } = updatedCase.attributes ?? {}; + const { connector, external_service, severity, ...restUpdateAttributes } = + updatedCase.attributes ?? {}; const transformedConnector = transformESConnectorToExternalModel({ // if the saved object had an error the attributes field will not exist @@ -68,6 +71,7 @@ export function transformUpdateResponseToExternalModel( ...updatedCase, attributes: { ...restUpdateAttributes, + ...((severity || severity === 0) && { severity: SEVERITY_ESMODEL_TO_EXTERNAL[severity] }), ...(transformedConnector && { connector: transformedConnector }), // if externalService is null that means we intentionally updated it to null within ES so return that as a valid value ...(externalService !== undefined && { external_service: externalService }), @@ -87,7 +91,7 @@ export function transformAttributesToESModel(caseAttributes: Partial; referenceHandler: ConnectorReferenceHandler; } { - const { connector, external_service, ...restAttributes } = caseAttributes; + const { connector, external_service, severity, ...restAttributes } = caseAttributes; const { connector_id: pushConnectorId, ...restExternalService } = external_service ?? {}; const transformedConnector = { @@ -113,6 +117,7 @@ export function transformAttributesToESModel(caseAttributes: Partial; +export enum ESCaseSeverity { + LOW = 0, + MEDIUM = 10, + HIGH = 20, + CRITICAL = 30, +} + /** * This type should only be used within the cases service and its helper functions (e.g. the transforms). * - * The type represents how the Cases object will be layed out in ES. It will not have connector.id or external_service.connector_id. - * Instead those fields will be transformed into the references field. + * The type represents how the Cases object will be layed out in ES. + * 1 - It will not have connector.id or external_service.connector_id. Instead those fields will be transformed into the references field. + * 2 - The Severity type is internally a number. */ -export type ESCaseAttributes = Omit & { +export type ESCaseAttributes = Omit< + CaseAttributes, + 'connector' | 'external_service' | 'severity' +> & { + severity: ESCaseSeverity; connector: ESCaseConnector; external_service: ExternalServicesWithoutConnectorId | null; }; diff --git a/x-pack/plugins/cases/server/services/test_utils.ts b/x-pack/plugins/cases/server/services/test_utils.ts index 70266c1a74635..e07bff709fe4c 100644 --- a/x-pack/plugins/cases/server/services/test_utils.ts +++ b/x-pack/plugins/cases/server/services/test_utils.ts @@ -18,6 +18,7 @@ import type { import { CaseSeverity, CaseStatuses, ConnectorTypes, NONE_CONNECTOR_ID } from '../../common/api'; import { CASE_SAVED_OBJECT, SECURITY_SOLUTION_OWNER } from '../../common/constants'; import type { ESCaseAttributes, ExternalServicesWithoutConnectorId } from './cases/types'; +import { ESCaseSeverity } from './cases/types'; import { getNoneCaseConnector } from '../common/utils'; /** @@ -96,6 +97,36 @@ export const createExternalService = ( ...overrides, }); +export const basicESCaseFields: ESCaseAttributes = { + closed_at: null, + closed_by: null, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + severity: ESCaseSeverity.LOW, + duration: null, + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + status: CaseStatuses.open, + tags: ['defacement'], + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + connector: getNoneCaseConnector(), + external_service: null, + settings: { + syncAlerts: true, + }, + owner: SECURITY_SOLUTION_OWNER, + assignees: [], +}; + export const basicCaseFields: CaseAttributes = { closed_at: null, closed_by: null, @@ -130,10 +161,12 @@ export const createCaseSavedObjectResponse = ({ connector, externalService, overrides, + caseId, }: { connector?: ESCaseConnectorWithId; externalService?: CaseFullExternalService; - overrides?: Partial; + overrides?: Partial; + caseId?: string; } = {}): SavedObject => { const references: SavedObjectReference[] = createSavedObjectReferences({ connector, @@ -165,9 +198,9 @@ export const createCaseSavedObjectResponse = ({ return { type: CASE_SAVED_OBJECT, - id: '1', + id: caseId ?? '1', attributes: { - ...basicCaseFields, + ...basicESCaseFields, ...overrides, // if connector is null we'll default this to an incomplete jira value because the service // should switch it to a none connector when the id can't be found in the references array diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts index 916e3c389232e..561fe9569fc96 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts @@ -267,6 +267,7 @@ export default ({ getService }: FtrProviderContext): void => { external_service: postedCase.external_service, owner: postedCase.owner, connector: postedCase.connector, + severity: postedCase.severity, comments: [], totalAlerts: 0, totalComment: 0, @@ -310,6 +311,34 @@ export default ({ getService }: FtrProviderContext): void => { }); } + it('sorts by severity', async () => { + const case4 = await createCase(supertest, { + ...postCaseReq, + severity: CaseSeverity.CRITICAL, + }); + const case3 = await createCase(supertest, { + ...postCaseReq, + severity: CaseSeverity.HIGH, + }); + const case2 = await createCase(supertest, { + ...postCaseReq, + severity: CaseSeverity.MEDIUM, + }); + const case1 = await createCase(supertest, { ...postCaseReq, severity: CaseSeverity.LOW }); + + const cases = await findCases({ + supertest, + query: { sortField: 'severity', sortOrder: 'asc' }, + }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 4, + cases: [case1, case2, case3, case4], + count_open_cases: 4, + }); + }); + describe('search and searchField', () => { beforeEach(async () => { await createCase(supertest, postCaseReq); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts index b988121dbf443..ee8e70d9cbeff 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts @@ -29,6 +29,7 @@ import { CaseStatuses, CaseSeverity, } from '@kbn/cases-plugin/common/api'; +import { ESCaseSeverity } from '@kbn/cases-plugin/server/services/cases/types'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { deleteAllCaseItems, @@ -206,7 +207,7 @@ const expectExportToHaveCaseSavedObject = ( expect(createdCaseSO.attributes.connector.fields).to.eql([]); expect(createdCaseSO.attributes.settings).to.eql(caseRequest.settings); expect(createdCaseSO.attributes.status).to.eql(CaseStatuses.open); - expect(createdCaseSO.attributes.severity).to.eql(CaseSeverity.LOW); + expect(createdCaseSO.attributes.severity).to.eql(ESCaseSeverity.LOW); expect(createdCaseSO.attributes.duration).to.eql(null); expect(createdCaseSO.attributes.tags).to.eql(caseRequest.tags); }; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts index 4a06a219392a0..49b0297fdde88 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { CASES_URL, SECURITY_SOLUTION_OWNER } from '@kbn/cases-plugin/common/constants'; import { AttributesTypeUser } from '@kbn/cases-plugin/common/api'; +import { ESCaseSeverity } from '@kbn/cases-plugin/server/services/cases/types'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { deleteAllCaseItems, @@ -476,5 +477,39 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); }); }); + + describe('8.7.0', () => { + before(async () => { + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/cases/8.5.0/cases_severity.json' + ); + }); + + after(async () => { + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/cases/8.5.0/cases_severity.json' + ); + await deleteAllCaseItems(es); + }); + + describe('severity', () => { + it('severity string labels are converted to matching number', async () => { + const expectedSeverityValues: Record = { + 'cases:063d5820-1284-11ed-81af-63a2bdfb2bf6': ESCaseSeverity.LOW, + 'cases:063d5820-1284-11ed-81af-63a2bdfb2bf7': ESCaseSeverity.MEDIUM, + 'cases:063d5820-1284-11ed-81af-63a2bdfb2bf8': ESCaseSeverity.HIGH, + 'cases:063d5820-1284-11ed-81af-63a2bdfb2bf9': ESCaseSeverity.CRITICAL, + }; + + const casesFromES = await getCaseSavedObjectsFromES({ es }); + + for (const hit of casesFromES.body.hits.hits) { + const caseID = hit._id; + expect(expectedSeverityValues[caseID]).not.to.be(undefined); + expect(hit._source?.cases.severity).to.eql(expectedSeverityValues[caseID]); + } + }); + }); + }); }); } diff --git a/x-pack/test/functional/fixtures/kbn_archiver/cases/8.5.0/cases_severity.json b/x-pack/test/functional/fixtures/kbn_archiver/cases/8.5.0/cases_severity.json new file mode 100644 index 0000000000000..1b2fac3f8655b --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/cases/8.5.0/cases_severity.json @@ -0,0 +1,191 @@ +{ + "attributes": { + "assignees": [ + { + "uid": "abc" + } + ], + "closed_at": null, + "closed_by": null, + "connector": { + "fields": [], + "name": "none", + "type": ".none" + }, + "created_at": "2022-08-02T16:56:16.806Z", + "created_by": { + "email": null, + "full_name": null, + "username": "elastic" + }, + "description": "a case description", + "duration": null, + "external_service": null, + "owner": "cases", + "settings": { + "syncAlerts": false + }, + "severity": "low", + "status": "open", + "tags": [ + "super", + "awesome" + ], + "title": "Test case", + "updated_at": null, + "updated_by": null + }, + "coreMigrationVersion": "8.5.0", + "id": "063d5820-1284-11ed-81af-63a2bdfb2bf6", + "migrationVersion": { + "cases": "8.5.0" + }, + "references": [], + "type": "cases", + "updated_at": "2022-08-02T16:56:16.808Z", + "version": "WzE3MywxXQ==" +} + +{ + "attributes": { + "assignees": [ + { + "uid": "abc" + } + ], + "closed_at": null, + "closed_by": null, + "connector": { + "fields": [], + "name": "none", + "type": ".none" + }, + "created_at": "2022-08-02T16:56:16.806Z", + "created_by": { + "email": null, + "full_name": null, + "username": "elastic" + }, + "description": "a case description", + "duration": null, + "external_service": null, + "owner": "cases", + "settings": { + "syncAlerts": false + }, + "severity": "medium", + "status": "open", + "tags": [ + "super", + "awesome" + ], + "title": "Test case", + "updated_at": null, + "updated_by": null + }, + "coreMigrationVersion": "8.5.0", + "id": "063d5820-1284-11ed-81af-63a2bdfb2bf7", + "migrationVersion": { + "cases": "8.5.0" + }, + "references": [], + "type": "cases", + "updated_at": "2022-08-02T16:56:16.808Z", + "version": "WzE3MywxXQ==" +} + +{ + "attributes": { + "assignees": [ + { + "uid": "abc" + } + ], + "closed_at": null, + "closed_by": null, + "connector": { + "fields": [], + "name": "none", + "type": ".none" + }, + "created_at": "2022-08-02T16:56:16.806Z", + "created_by": { + "email": null, + "full_name": null, + "username": "elastic" + }, + "description": "a case description", + "duration": null, + "external_service": null, + "owner": "cases", + "settings": { + "syncAlerts": false + }, + "severity": "high", + "status": "open", + "tags": [ + "super", + "awesome" + ], + "title": "Test case", + "updated_at": null, + "updated_by": null + }, + "coreMigrationVersion": "8.5.0", + "id": "063d5820-1284-11ed-81af-63a2bdfb2bf8", + "migrationVersion": { + "cases": "8.5.0" + }, + "references": [], + "type": "cases", + "updated_at": "2022-08-02T16:56:16.808Z", + "version": "WzE3MywxXQ==" +} + +{ + "attributes": { + "assignees": [ + { + "uid": "abc" + } + ], + "closed_at": null, + "closed_by": null, + "connector": { + "fields": [], + "name": "none", + "type": ".none" + }, + "created_at": "2022-08-02T16:56:16.806Z", + "created_by": { + "email": null, + "full_name": null, + "username": "elastic" + }, + "description": "a case description", + "duration": null, + "external_service": null, + "owner": "cases", + "settings": { + "syncAlerts": false + }, + "severity": "critical", + "status": "open", + "tags": [ + "super", + "awesome" + ], + "title": "Test case", + "updated_at": null, + "updated_by": null + }, + "coreMigrationVersion": "8.5.0", + "id": "063d5820-1284-11ed-81af-63a2bdfb2bf9", + "migrationVersion": { + "cases": "8.5.0" + }, + "references": [], + "type": "cases", + "updated_at": "2022-08-02T16:56:16.808Z", + "version": "WzE3MywxXQ==" +} From f83c49b27553933631c33d5de9da37614e784a6b Mon Sep 17 00:00:00 2001 From: GitStart <1501599+gitstart@users.noreply.github.com> Date: Mon, 19 Dec 2022 17:27:52 +0100 Subject: [PATCH 17/55] [Dashboard] Labs Flyout Not Working (#146771) Fixed opening the Labs flyout on dashboard creates an error. --- .../dashboard_app/top_nav/dashboard_top_nav.tsx | 11 +++++++++-- src/plugins/presentation_util/public/index.ts | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx index f4e0fa3b44f5e..2181a66122ae4 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx @@ -9,10 +9,14 @@ import UseUnmount from 'react-use/lib/useUnmount'; import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { + withSuspense, + LazyLabsFlyout, + getContextProvider as getPresentationUtilContextProvider, +} from '@kbn/presentation-util-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { TopNavMenuProps } from '@kbn/navigation-plugin/public'; -import { withSuspense, LazyLabsFlyout } from '@kbn/presentation-util-plugin/public'; import { getDashboardTitle, @@ -74,6 +78,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr embeddableInstance: dashboardContainer, } = useDashboardContainerContext(); const dispatch = useEmbeddableDispatch(); + const PresentationUtilContextProvider = getPresentationUtilContextProvider(); const hasUnsavedChanges = select((state) => state.componentState.hasUnsavedChanges); const fullScreenMode = select((state) => state.componentState.fullScreenMode); @@ -251,7 +256,9 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr >{`${getDashboardBreadcrumb()} - ${dashboardTitle}`} {viewMode !== ViewMode.PRINT && isLabsEnabled && isLabsShown ? ( - setIsLabsShown(false)} /> + + setIsLabsShown(false)} /> + ) : null} {viewMode === ViewMode.EDIT ? : null} diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts index d98bf8aa05d73..504363208d6a0 100644 --- a/src/plugins/presentation_util/public/index.ts +++ b/src/plugins/presentation_util/public/index.ts @@ -90,3 +90,5 @@ export function plugin() { } export const useLabs = () => (() => pluginServices.getHooks().labs.useService())(); + +export const getContextProvider = () => pluginServices.getContextProvider(); From 794e721cc0a467f8afcbde884a0a782a6399abaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Mon, 19 Dec 2022 11:31:09 -0500 Subject: [PATCH 18/55] Exclude muted alerts from alert summaries (#147664) Resolves https://github.com/elastic/kibana/issues/147531. In this PR, I'm making the alert summary actions exclude muted alerts. ## To verify **Scenario 1 (summary per rule run)** 1. Install sample web logs by visiting `/app/home#/tutorial_directory/sampleData`, clicking `Other sample data sets` and clicking `Add data` for `Sample web logs` 2. Add `kibana_sample_data_logs` index pattern to O11y settings by visiting `/app/metrics/explorer`, clicking `Settings` on the top right, appending `,kibana_sample_data_logs` (with leading comma) to the `Metrics indices` field and clicking `Apply`. 3. Create a metric threshold rule that generates multiple alerts by using the following curl command (fix url) ``` curl -XPOST -H "Content-type: application/json" -H "kbn-xsrf: foo" -d '{"params":{"criteria":[{"metric":"bytes","comparator":">","threshold":[0],"timeSize":1,"timeUnit":"h","aggType":"avg"}],"sourceId":"default","alertOnNoData":true,"alertOnGroupDisappear":true,"groupBy":["agent.keyword"]},"consumer":"infrastructure","schedule":{"interval":"10s"},"tags":[],"name":"test","rule_type_id":"metrics.alert.threshold","actions":[{"frequency":{"summary":true,"notify_when":"onActiveAlert"},"group":"metrics.threshold.fired","id":"preconfigured-server-log","params":{"level":"info","message":"Found {{alerts.all.count}} alerts. {{alerts.new.count}} new, {{alerts.ongoing.count}} ongoing, {{alerts.recovered.count}} recovered."}}]}' 'http://elastic:changeme@localhost:5601/api/alerting/rule' ``` 4. Observe 3 alerts in the summary (new then ongoing) 5. Mute one of the alerts by using the rule details page alerts tab 6. Observe only 2 alerts are now in the summary **Scenario 2 (summary over a time spam)** Same steps as above except for step 3, use the following curl command (summaries will generate every 30s) ``` curl -XPOST -H "Content-type: application/json" -H "kbn-xsrf: foo" -d '{"params":{"criteria":[{"metric":"bytes","comparator":">","threshold":[0],"timeSize":1,"timeUnit":"h","aggType":"avg"}],"sourceId":"default","alertOnNoData":true,"alertOnGroupDisappear":true,"groupBy":["agent.keyword"]},"consumer":"infrastructure","schedule":{"interval":"10s"},"tags":[],"name":"test","rule_type_id":"metrics.alert.threshold","actions":[{"frequency":{"summary":true,"notify_when":"onThrottleInterval","throttle":"30s"},"group":"metrics.threshold.fired","id":"preconfigured-server-log","params":{"level":"info","message":"Found {{alerts.all.count}} alerts. {{alerts.new.count}} new, {{alerts.ongoing.count}} ongoing, {{alerts.recovered.count}} recovered."}}]}' 'http://elastic:changeme@localhost:5601/api/alerting/rule' ``` --- .../task_runner/execution_handler.test.ts | 4 + .../server/task_runner/execution_handler.ts | 8 +- .../server/task_runner/task_runner.test.ts | 2 + x-pack/plugins/alerting/server/types.ts | 1 + .../create_get_summarized_alerts_fn.test.ts | 118 +++++++++++++++++- .../utils/create_get_summarized_alerts_fn.ts | 106 +++++++++++++--- .../tests/trial/get_summarized_alerts.ts | 115 +++++++++++++++-- 7 files changed, 323 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts index b46f3ddb38236..ed88a9ecb60c1 100644 --- a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts @@ -846,6 +846,7 @@ describe('Execution Handler', () => { generateExecutionParams({ rule: { ...defaultExecutionParams.rule, + mutedInstanceIds: ['foo'], actions: [ { id: '1', @@ -872,6 +873,7 @@ describe('Execution Handler', () => { executionUuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', ruleId: '1', spaceId: 'test1', + excludedAlertInstanceIds: ['foo'], }); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.bulkEnqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` @@ -959,6 +961,7 @@ describe('Execution Handler', () => { generateExecutionParams({ rule: { ...defaultExecutionParams.rule, + mutedInstanceIds: ['foo'], actions: [ { id: '1', @@ -986,6 +989,7 @@ describe('Execution Handler', () => { end: new Date(), ruleId: '1', spaceId: 'test1', + excludedAlertInstanceIds: ['foo'], }); expect(result).toEqual({ throttledActions: { diff --git a/x-pack/plugins/alerting/server/task_runner/execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/execution_handler.ts index e6575b0d2f8ed..8755d480621d7 100644 --- a/x-pack/plugins/alerting/server/task_runner/execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/execution_handler.ts @@ -201,7 +201,11 @@ export class ExecutionHandler< if (isSummaryActionPerRuleRun(action) && !this.hasAlerts(alerts)) { continue; } - const summarizedAlerts = await this.getSummarizedAlerts({ action, spaceId, ruleId }); + const summarizedAlerts = await this.getSummarizedAlerts({ + action, + spaceId, + ruleId, + }); const actionToRun = { ...action, params: injectActionParams({ @@ -525,12 +529,14 @@ export class ExecutionHandler< end: new Date(), ruleId, spaceId, + excludedAlertInstanceIds: this.rule.mutedInstanceIds, }; } else { options = { executionUuid: this.executionId, ruleId, spaceId, + excludedAlertInstanceIds: this.rule.mutedInstanceIds, }; } diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 8b81b8c1146cd..c39c6200952ab 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -1370,6 +1370,7 @@ describe('Task Runner', () => { executionUuid: '5f6aa57d-3e22-484e-bae8-cbed868f4d28', ruleId: '1', spaceId: 'default', + excludedAlertInstanceIds: [], }); expect(enqueueFunction).toHaveBeenCalledTimes(1); expect(enqueueFunction).toHaveBeenCalledWith( @@ -1445,6 +1446,7 @@ describe('Task Runner', () => { end: new Date(DATE_1970), ruleId: '1', spaceId: 'default', + excludedAlertInstanceIds: [], }); expect(enqueueFunction).toHaveBeenCalledTimes(1); expect(enqueueFunction).toHaveBeenCalledWith( diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 06b53219b2e80..3cd3807649f2e 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -131,6 +131,7 @@ export interface GetSummarizedAlertsFnOpts { executionUuid?: string; ruleId: string; spaceId: string; + excludedAlertInstanceIds: string[]; } // TODO - add type for these alerts when we determine which alerts-as-data diff --git a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts index c34dfbc0b7f2a..0ceb1e6ed8a84 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts @@ -43,7 +43,12 @@ describe('createGetSummarizedAlertsFn', () => { isLifecycleAlert: false, })(); - await getSummarizedAlertsFn({ executionUuid: 'abc', ruleId: 'rule-id', spaceId: 'space-id' }); + await getSummarizedAlertsFn({ + executionUuid: 'abc', + ruleId: 'rule-id', + spaceId: 'space-id', + excludedAlertInstanceIds: [], + }); expect(ruleDataClientMock.getReader).toHaveBeenCalledWith({ namespace: 'space-id' }); }); @@ -54,7 +59,12 @@ describe('createGetSummarizedAlertsFn', () => { isLifecycleAlert: false, })(); - await getSummarizedAlertsFn({ executionUuid: 'abc', ruleId: 'rule-id', spaceId: 'space-id' }); + await getSummarizedAlertsFn({ + executionUuid: 'abc', + ruleId: 'rule-id', + spaceId: 'space-id', + excludedAlertInstanceIds: [], + }); expect(ruleDataClientMock.getReader).toHaveBeenCalledWith(); }); @@ -156,6 +166,7 @@ describe('createGetSummarizedAlertsFn', () => { executionUuid: 'abc', ruleId: 'rule-id', spaceId: 'space-id', + excludedAlertInstanceIds: ['TEST_ALERT_10'], }); expect(ruleDataClientMock.getReader).toHaveBeenCalledWith(); expect(ruleDataClientMock.getReader().search).toHaveBeenCalledTimes(3); @@ -181,6 +192,15 @@ describe('createGetSummarizedAlertsFn', () => { [EVENT_ACTION]: 'open', }, }, + { + bool: { + must_not: { + terms: { + [ALERT_INSTANCE_ID]: ['TEST_ALERT_10'], + }, + }, + }, + }, ], }, }, @@ -208,6 +228,15 @@ describe('createGetSummarizedAlertsFn', () => { [EVENT_ACTION]: 'active', }, }, + { + bool: { + must_not: { + terms: { + [ALERT_INSTANCE_ID]: ['TEST_ALERT_10'], + }, + }, + }, + }, ], }, }, @@ -235,6 +264,15 @@ describe('createGetSummarizedAlertsFn', () => { [EVENT_ACTION]: 'close', }, }, + { + bool: { + must_not: { + terms: { + [ALERT_INSTANCE_ID]: ['TEST_ALERT_10'], + }, + }, + }, + }, ], }, }, @@ -405,6 +443,7 @@ describe('createGetSummarizedAlertsFn', () => { end: new Date('2020-01-01T12:25:00.000Z'), ruleId: 'rule-id', spaceId: 'space-id', + excludedAlertInstanceIds: ['TEST_ALERT_10'], }); expect(ruleDataClientMock.getReader).toHaveBeenCalledWith(); expect(ruleDataClientMock.getReader().search).toHaveBeenCalledTimes(3); @@ -428,6 +467,15 @@ describe('createGetSummarizedAlertsFn', () => { [ALERT_RULE_UUID]: 'rule-id', }, }, + { + bool: { + must_not: { + terms: { + [ALERT_INSTANCE_ID]: ['TEST_ALERT_10'], + }, + }, + }, + }, { range: { [ALERT_START]: { @@ -460,6 +508,15 @@ describe('createGetSummarizedAlertsFn', () => { [ALERT_RULE_UUID]: 'rule-id', }, }, + { + bool: { + must_not: { + terms: { + [ALERT_INSTANCE_ID]: ['TEST_ALERT_10'], + }, + }, + }, + }, { range: { [ALERT_START]: { @@ -501,6 +558,15 @@ describe('createGetSummarizedAlertsFn', () => { [ALERT_RULE_UUID]: 'rule-id', }, }, + { + bool: { + must_not: { + terms: { + [ALERT_INSTANCE_ID]: ['TEST_ALERT_10'], + }, + }, + }, + }, { range: { [ALERT_END]: { @@ -655,6 +721,7 @@ describe('createGetSummarizedAlertsFn', () => { executionUuid: 'abc', ruleId: 'rule-id', spaceId: 'space-id', + excludedAlertInstanceIds: ['TEST_ALERT_10'], }); expect(ruleDataClientMock.getReader).toHaveBeenCalledWith({ namespace: 'space-id' }); expect(ruleDataClientMock.getReader().search).toHaveBeenCalledTimes(1); @@ -675,6 +742,15 @@ describe('createGetSummarizedAlertsFn', () => { [ALERT_RULE_UUID]: 'rule-id', }, }, + { + bool: { + must_not: { + terms: { + [ALERT_INSTANCE_ID]: ['TEST_ALERT_10'], + }, + }, + }, + }, ], }, }, @@ -807,6 +883,7 @@ describe('createGetSummarizedAlertsFn', () => { end: new Date('2020-01-01T12:25:00.000Z'), ruleId: 'rule-id', spaceId: 'space-id', + excludedAlertInstanceIds: ['TEST_ALERT_10'], }); expect(ruleDataClientMock.getReader).toHaveBeenCalledWith({ namespace: 'space-id' }); expect(ruleDataClientMock.getReader().search).toHaveBeenCalledTimes(1); @@ -830,6 +907,15 @@ describe('createGetSummarizedAlertsFn', () => { [ALERT_RULE_UUID]: 'rule-id', }, }, + { + bool: { + must_not: { + terms: { + [ALERT_INSTANCE_ID]: ['TEST_ALERT_10'], + }, + }, + }, + }, ], }, }, @@ -897,7 +983,12 @@ describe('createGetSummarizedAlertsFn', () => { })(); await expect( - getSummarizedAlertsFn({ executionUuid: 'abc', ruleId: 'rule-id', spaceId: 'space-id' }) + getSummarizedAlertsFn({ + executionUuid: 'abc', + ruleId: 'rule-id', + spaceId: 'space-id', + excludedAlertInstanceIds: [], + }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"search error"`); }); @@ -909,7 +1000,11 @@ describe('createGetSummarizedAlertsFn', () => { })(); await expect( - getSummarizedAlertsFn({ ruleId: 'rule-id', spaceId: 'space-id' }) + getSummarizedAlertsFn({ + ruleId: 'rule-id', + spaceId: 'space-id', + excludedAlertInstanceIds: [], + }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Must specify either execution UUID or time range for summarized alert query."` ); @@ -929,6 +1024,7 @@ describe('createGetSummarizedAlertsFn', () => { end: new Date(), ruleId: 'rule-id', spaceId: 'space-id', + excludedAlertInstanceIds: [], }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Must specify either execution UUID or time range for summarized alert query."` @@ -943,7 +1039,12 @@ describe('createGetSummarizedAlertsFn', () => { })(); await expect( - getSummarizedAlertsFn({ start: new Date(), ruleId: 'rule-id', spaceId: 'space-id' }) + getSummarizedAlertsFn({ + start: new Date(), + ruleId: 'rule-id', + spaceId: 'space-id', + excludedAlertInstanceIds: [], + }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Must specify either execution UUID or time range for summarized alert query."` ); @@ -957,7 +1058,12 @@ describe('createGetSummarizedAlertsFn', () => { })(); await expect( - getSummarizedAlertsFn({ end: new Date(), ruleId: 'rule-id', spaceId: 'space-id' }) + getSummarizedAlertsFn({ + end: new Date(), + ruleId: 'rule-id', + spaceId: 'space-id', + excludedAlertInstanceIds: [], + }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Must specify either execution UUID or time range for summarized alert query."` ); diff --git a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts index 037fb93bf7cfb..82d044ad65a68 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts @@ -15,6 +15,7 @@ import { ALERT_START, EVENT_ACTION, TIMESTAMP, + ALERT_INSTANCE_ID, } from '@kbn/rule-data-utils'; import { QueryDslQueryContainer, @@ -36,7 +37,14 @@ interface CreateGetSummarizedAlertsFnOpts { export const createGetSummarizedAlertsFn = (opts: CreateGetSummarizedAlertsFnOpts) => () => - async ({ start, end, executionUuid, ruleId, spaceId }: GetSummarizedAlertsFnOpts) => { + async ({ + start, + end, + executionUuid, + ruleId, + spaceId, + excludedAlertInstanceIds, + }: GetSummarizedAlertsFnOpts) => { if (!ruleId || !spaceId) { throw new Error(`Must specify both rule ID and space ID for summarized alert query.`); } @@ -64,6 +72,7 @@ export const createGetSummarizedAlertsFn = ruleId, executionUuid: executionUuid!, isLifecycleAlert: opts.isLifecycleAlert, + excludedAlertInstanceIds, }); } @@ -73,6 +82,7 @@ export const createGetSummarizedAlertsFn = start: start!, end: end!, isLifecycleAlert: opts.isLifecycleAlert, + excludedAlertInstanceIds, }); }; @@ -81,6 +91,7 @@ interface GetAlertsByExecutionUuidOpts { ruleId: string; ruleDataClientReader: IRuleDataReader; isLifecycleAlert: boolean; + excludedAlertInstanceIds: string[]; } const getAlertsByExecutionUuid = async ({ @@ -88,28 +99,41 @@ const getAlertsByExecutionUuid = async ({ ruleId, ruleDataClientReader, isLifecycleAlert, + excludedAlertInstanceIds, }: GetAlertsByExecutionUuidOpts) => { if (isLifecycleAlert) { - return getLifecycleAlertsByExecutionUuid({ executionUuid, ruleId, ruleDataClientReader }); + return getLifecycleAlertsByExecutionUuid({ + executionUuid, + ruleId, + ruleDataClientReader, + excludedAlertInstanceIds, + }); } - return getPersistentAlertsByExecutionUuid({ executionUuid, ruleId, ruleDataClientReader }); + return getPersistentAlertsByExecutionUuid({ + executionUuid, + ruleId, + ruleDataClientReader, + excludedAlertInstanceIds, + }); }; interface GetAlertsByExecutionUuidHelperOpts { executionUuid: string; ruleId: string; ruleDataClientReader: IRuleDataReader; + excludedAlertInstanceIds: string[]; } const getPersistentAlertsByExecutionUuid = async ({ executionUuid, ruleId, ruleDataClientReader, + excludedAlertInstanceIds, }: GetAlertsByExecutionUuidHelperOpts) => { // persistent alerts only create new alerts so query by execution UUID to // get all alerts created during an execution - const request = getQueryByExecutionUuid(executionUuid, ruleId); + const request = getQueryByExecutionUuid(executionUuid, ruleId, excludedAlertInstanceIds); const response = (await ruleDataClientReader.search(request)) as ESSearchResponse< AlertDocument, TSearchRequest @@ -132,15 +156,16 @@ const getLifecycleAlertsByExecutionUuid = async ({ executionUuid, ruleId, ruleDataClientReader, + excludedAlertInstanceIds, }: GetAlertsByExecutionUuidHelperOpts) => { // lifecycle alerts assign a different action to an alert depending // on whether it is new/ongoing/recovered. query for each action in order // to get the count of each action type as well as up to the maximum number // of each type of alert. const requests = [ - getQueryByExecutionUuid(executionUuid, ruleId, 'open'), - getQueryByExecutionUuid(executionUuid, ruleId, 'active'), - getQueryByExecutionUuid(executionUuid, ruleId, 'close'), + getQueryByExecutionUuid(executionUuid, ruleId, excludedAlertInstanceIds, 'open'), + getQueryByExecutionUuid(executionUuid, ruleId, excludedAlertInstanceIds, 'active'), + getQueryByExecutionUuid(executionUuid, ruleId, excludedAlertInstanceIds, 'close'), ]; const responses = await Promise.all( @@ -163,7 +188,12 @@ const getHitsWithCount = ( }; }; -const getQueryByExecutionUuid = (executionUuid: string, ruleId: string, action?: string) => { +const getQueryByExecutionUuid = ( + executionUuid: string, + ruleId: string, + excludedAlertInstanceIds: string[], + action?: string +) => { const filter: QueryDslQueryContainer[] = [ { term: { @@ -183,6 +213,17 @@ const getQueryByExecutionUuid = (executionUuid: string, ruleId: string, action?: }, }); } + if (excludedAlertInstanceIds.length) { + filter.push({ + bool: { + must_not: { + terms: { + [ALERT_INSTANCE_ID]: excludedAlertInstanceIds, + }, + }, + }, + }); + } return { body: { @@ -203,6 +244,7 @@ interface GetAlertsByTimeRangeOpts { ruleId: string; ruleDataClientReader: IRuleDataReader; isLifecycleAlert: boolean; + excludedAlertInstanceIds: string[]; } const getAlertsByTimeRange = async ({ @@ -211,12 +253,25 @@ const getAlertsByTimeRange = async ({ ruleId, ruleDataClientReader, isLifecycleAlert, + excludedAlertInstanceIds, }: GetAlertsByTimeRangeOpts) => { if (isLifecycleAlert) { - return getLifecycleAlertsByTimeRange({ start, end, ruleId, ruleDataClientReader }); + return getLifecycleAlertsByTimeRange({ + start, + end, + ruleId, + ruleDataClientReader, + excludedAlertInstanceIds, + }); } - return getPersistentAlertsByTimeRange({ start, end, ruleId, ruleDataClientReader }); + return getPersistentAlertsByTimeRange({ + start, + end, + ruleId, + ruleDataClientReader, + excludedAlertInstanceIds, + }); }; interface GetAlertsByTimeRangeHelperOpts { @@ -224,6 +279,7 @@ interface GetAlertsByTimeRangeHelperOpts { end: Date; ruleId: string; ruleDataClientReader: IRuleDataReader; + excludedAlertInstanceIds: string[]; } enum AlertTypes { @@ -237,10 +293,11 @@ const getPersistentAlertsByTimeRange = async { // persistent alerts only create new alerts so query for all alerts within the time // range and treat them as NEW - const request = getQueryByTimeRange(start, end, ruleId); + const request = getQueryByTimeRange(start, end, ruleId, excludedAlertInstanceIds); const response = (await ruleDataClientReader.search(request)) as ESSearchResponse< AlertDocument, TSearchRequest @@ -264,11 +321,12 @@ const getLifecycleAlertsByTimeRange = async ({ end, ruleId, ruleDataClientReader, + excludedAlertInstanceIds, }: GetAlertsByTimeRangeHelperOpts) => { const requests = [ - getQueryByTimeRange(start, end, ruleId, AlertTypes.NEW), - getQueryByTimeRange(start, end, ruleId, AlertTypes.ONGOING), - getQueryByTimeRange(start, end, ruleId, AlertTypes.RECOVERED), + getQueryByTimeRange(start, end, ruleId, excludedAlertInstanceIds, AlertTypes.NEW), + getQueryByTimeRange(start, end, ruleId, excludedAlertInstanceIds, AlertTypes.ONGOING), + getQueryByTimeRange(start, end, ruleId, excludedAlertInstanceIds, AlertTypes.RECOVERED), ]; const responses = await Promise.all( @@ -282,7 +340,13 @@ const getLifecycleAlertsByTimeRange = async ({ }; }; -const getQueryByTimeRange = (start: Date, end: Date, ruleId: string, type?: AlertTypes) => { +const getQueryByTimeRange = ( + start: Date, + end: Date, + ruleId: string, + excludedAlertInstanceIds: string[], + type?: AlertTypes +) => { // base query filters the alert documents for a rule by the given time range let filter: QueryDslQueryContainer[] = [ { @@ -300,6 +364,18 @@ const getQueryByTimeRange = (start: Date, end: Date, ruleId: string, type?: Aler }, ]; + if (excludedAlertInstanceIds.length) { + filter.push({ + bool: { + must_not: { + terms: { + [ALERT_INSTANCE_ID]: excludedAlertInstanceIds, + }, + }, + }, + }); + } + if (type === AlertTypes.NEW) { // alerts are considered NEW within the time range if they started after // the query start time diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts index cc72f48d31613..8f50fd4589769 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts @@ -14,6 +14,7 @@ import { mappingFromFieldMap } from '@kbn/rule-registry-plugin/common/mapping_fr import { AlertConsumers, ALERT_REASON, + ALERT_INSTANCE_ID, } from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names'; import { createLifecycleExecutor, @@ -192,13 +193,11 @@ export default function createGetSummarizedAlertsTest({ getService }: FtrProvide executionId: execution1Uuid, }); - // Refresh the index so the data is available for reading - await es.indices.refresh({ index: `${ruleDataClient.indexName}*` }); - const execution1SummarizedAlerts = await getSummarizedAlerts({ ruleId: id, executionUuid: execution1Uuid, spaceId: 'default', + excludedAlertInstanceIds: [], }); expect(execution1SummarizedAlerts.new.count).to.eql(1); expect(execution1SummarizedAlerts.ongoing.count).to.eql(0); @@ -214,13 +213,11 @@ export default function createGetSummarizedAlertsTest({ getService }: FtrProvide executionId: execution2Uuid, }); - // Refresh the index so the data is available for reading - await es.indices.refresh({ index: `${ruleDataClient.indexName}*` }); - const execution2SummarizedAlerts = await getSummarizedAlerts({ ruleId: id, executionUuid: execution2Uuid, spaceId: 'default', + excludedAlertInstanceIds: [], }); expect(execution2SummarizedAlerts.new.count).to.eql(0); expect(execution2SummarizedAlerts.ongoing.count).to.eql(1); @@ -235,13 +232,11 @@ export default function createGetSummarizedAlertsTest({ getService }: FtrProvide executionId: execution3Uuid, }); - // Refresh the index so the data is available for reading - await es.indices.refresh({ index: `${ruleDataClient.indexName}*` }); - const execution3SummarizedAlerts = await getSummarizedAlerts({ ruleId: id, executionUuid: execution3Uuid, spaceId: 'default', + excludedAlertInstanceIds: [], }); expect(execution3SummarizedAlerts.new.count).to.eql(0); expect(execution3SummarizedAlerts.ongoing.count).to.eql(0); @@ -255,6 +250,7 @@ export default function createGetSummarizedAlertsTest({ getService }: FtrProvide start: preExecution1Start, end: new Date(), spaceId: 'default', + excludedAlertInstanceIds: [], }); expect(timeRangeSummarizedAlerts1.new.count).to.eql(1); expect(timeRangeSummarizedAlerts1.ongoing.count).to.eql(0); @@ -268,10 +264,111 @@ export default function createGetSummarizedAlertsTest({ getService }: FtrProvide start: preExecution2Start, end: new Date(), spaceId: 'default', + excludedAlertInstanceIds: [], }); expect(timeRangeSummarizedAlerts2.new.count).to.eql(0); expect(timeRangeSummarizedAlerts2.ongoing.count).to.eql(0); expect(timeRangeSummarizedAlerts2.recovered.count).to.eql(1); }); + + it(`shouldn't return muted alerts`, async () => { + const ruleId = uuid.v4(); + const id1 = 'host-01'; + const id2 = 'host-02'; + + // This creates the function that will wrap the solution's rule executor with the RuleRegistry lifecycle + const createLifecycleRuleExecutor = createLifecycleExecutor(logger, ruleDataClient); + const createGetSummarizedAlerts = createGetSummarizedAlertsFn({ + ruleDataClient, + useNamespace: false, + isLifecycleAlert: true, + }); + + // This creates the executor that is passed to the Alerting framework. + const executor = createLifecycleRuleExecutor< + MockRuleParams, + { shouldTriggerAlert: boolean }, + MockAlertState, + MockAlertContext, + MockAllowedActionGroups + >(async function (options) { + const { services } = options; + const { alertWithLifecycle } = services; + + alertWithLifecycle({ + id: id1, + fields: { + [ALERT_REASON]: 'Test alert is firing', + }, + }); + alertWithLifecycle({ + id: id2, + fields: { + [ALERT_REASON]: 'Test alert is firing', + }, + }); + }); + + const getSummarizedAlerts = createGetSummarizedAlerts(); + + // Create the options with the minimal amount of values to test the lifecycle executor + const options = { + spaceId: 'default', + rule: { + id: ruleId, + name: 'test rule', + ruleTypeId: 'observability.test.fake', + ruleTypeName: 'test', + consumer: 'observability', + producer: 'observability.test', + }, + services: { + alertFactory: { create: sinon.stub() }, + shouldWriteAlerts: sinon.stub().returns(true), + }, + } as unknown as RuleExecutorOptions< + MockRuleParams, + WrappedLifecycleRuleState<{ shouldTriggerAlert: boolean }>, + { [x: string]: unknown }, + { [x: string]: unknown }, + string + >; + + const getState = ( + shouldTriggerAlert: boolean, + alerts: Record + ) => ({ wrapped: { shouldTriggerAlert }, trackedAlerts: alerts, trackedAlertsRecovered: {} }); + + // Execute the rule the first time - this creates a new alert + const execution1Uuid = uuid.v4(); + await executor({ + ...options, + startedAt: new Date(), + state: getState(true, {}), + executionId: execution1Uuid, + }); + + const summarizedAlertsExcludingId1 = await getSummarizedAlerts({ + ruleId, + executionUuid: execution1Uuid, + spaceId: 'default', + excludedAlertInstanceIds: [id1], + }); + expect(summarizedAlertsExcludingId1.new.count).to.eql(1); + expect(summarizedAlertsExcludingId1.ongoing.count).to.eql(0); + expect(summarizedAlertsExcludingId1.recovered.count).to.eql(0); + expect(summarizedAlertsExcludingId1.new.data[0][ALERT_INSTANCE_ID]).to.eql(id2); + + const summarizedAlertsExcludingId2 = await getSummarizedAlerts({ + ruleId, + executionUuid: execution1Uuid, + spaceId: 'default', + excludedAlertInstanceIds: [id2], + }); + expect(summarizedAlertsExcludingId2.new.count).to.eql(1); + expect(summarizedAlertsExcludingId2.ongoing.count).to.eql(0); + expect(summarizedAlertsExcludingId2.recovered.count).to.eql(0); + expect(summarizedAlertsExcludingId2.new.data[0][ALERT_INSTANCE_ID]).to.eql(id1); + }); }); } From b00a2643cd7a2980902ee693d67cc6b801daacab Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 19 Dec 2022 18:24:26 +0100 Subject: [PATCH 19/55] [@kbn/handlebars] Code cleanup (#147425) --- packages/kbn-handlebars/.patches/blocks.patch | 18 +- .../kbn-handlebars/.patches/builtins.patch | 71 +-- .../kbn-handlebars/.patches/compiler.patch | 55 ++- packages/kbn-handlebars/.patches/data.patch | 107 ++--- .../kbn-handlebars/.patches/helpers.patch | 442 +++++++++--------- packages/kbn-handlebars/README.md | 9 +- .../__snapshots__/index.test.ts.snap | 18 - packages/kbn-handlebars/index.test.ts | 51 +- packages/kbn-handlebars/index.ts | 42 +- .../kbn-handlebars/src/__jest__/test_bench.ts | 30 +- .../src/upstream/index.blocks.test.ts | 4 + .../src/upstream/index.builtins.test.ts | 2 + .../src/upstream/index.compiler.test.ts | 13 +- .../src/upstream/index.data.test.ts | 2 + .../src/upstream/index.helpers.test.ts | 4 + 15 files changed, 437 insertions(+), 431 deletions(-) diff --git a/packages/kbn-handlebars/.patches/blocks.patch b/packages/kbn-handlebars/.patches/blocks.patch index f5c68780a3654..9acf633c1b5f5 100644 --- a/packages/kbn-handlebars/.patches/blocks.patch +++ b/packages/kbn-handlebars/.patches/blocks.patch @@ -429,15 +429,19 @@ > beforeEach(() => { > global.kbnHandlebarsEnv = Handlebars.create(); > }); -410c319,323 +410c319,327 < handlebarsEnv.registerDecorator('foo', function() { --- +> afterEach(() => { +> global.kbnHandlebarsEnv = null; +> }); +> > it('unregisters', () => { > // @ts-expect-error: Cannot assign to 'decorators' because it is a read-only property. > kbnHandlebarsEnv!.decorators = {}; > > kbnHandlebarsEnv!.registerDecorator('foo', function () { -414,416c327,329 +414,416c331,333 < equals(!!handlebarsEnv.decorators.foo, true); < handlebarsEnv.unregisterDecorator('foo'); < equals(handlebarsEnv.decorators.foo, undefined); @@ -445,14 +449,14 @@ > expect(!!kbnHandlebarsEnv!.decorators.foo).toEqual(true); > kbnHandlebarsEnv!.unregisterDecorator('foo'); > expect(kbnHandlebarsEnv!.decorators.foo).toBeUndefined(); -419,420c332,334 +419,420c336,338 < it('allows multiple globals', function() { < handlebarsEnv.decorators = {}; --- > it('allows multiple globals', () => { > // @ts-expect-error: Cannot assign to 'decorators' because it is a read-only property. > kbnHandlebarsEnv!.decorators = {}; -422,424c336,339 +422,424c340,343 < handlebarsEnv.registerDecorator({ < foo: function() {}, < bar: function() {} @@ -461,7 +465,7 @@ > kbnHandlebarsEnv!.registerDecorator({ > foo() {}, > bar() {}, -427,432c342,347 +427,432c346,351 < equals(!!handlebarsEnv.decorators.foo, true); < equals(!!handlebarsEnv.decorators.bar, true); < handlebarsEnv.unregisterDecorator('foo'); @@ -475,7 +479,7 @@ > kbnHandlebarsEnv!.unregisterDecorator('bar'); > expect(kbnHandlebarsEnv!.decorators.foo).toBeUndefined(); > expect(kbnHandlebarsEnv!.decorators.bar).toBeUndefined(); -435,445c350,356 +435,445c354,360 < it('fails with multiple and args', function() { < shouldThrow( < function() { @@ -495,7 +499,7 @@ > { > world() { > return 'world!'; -447,452c358,364 +447,452c362,368 < {} < ); < }, diff --git a/packages/kbn-handlebars/.patches/builtins.patch b/packages/kbn-handlebars/.patches/builtins.patch index 536c1ee8d7da3..30b40456c2142 100644 --- a/packages/kbn-handlebars/.patches/builtins.patch +++ b/packages/kbn-handlebars/.patches/builtins.patch @@ -668,7 +668,10 @@ --- > > afterEach(function () { -578,580c481,484 +575a479,480 +> +> global.kbnHandlebarsEnv = null; +578,580c483,486 < it('should call logger at default level', function() { < var levelArg, logArg; < handlebarsEnv.log = function(level, arg) { @@ -677,7 +680,7 @@ > let levelArg; > let logArg; > kbnHandlebarsEnv!.log = function (level, arg) { -585,590c489,491 +585,590c491,493 < expectTemplate('{{log blah}}') < .withInput({ blah: 'whee' }) < .withMessage('log should not display') @@ -688,7 +691,7 @@ > expectTemplate('{{log blah}}').withInput({ blah: 'whee' }).toCompileTo(''); > expect(1).toEqual(levelArg); > expect('whee').toEqual(logArg); -593,595c494,497 +593,595c496,499 < it('should call logger at data level', function() { < var levelArg, logArg; < handlebarsEnv.log = function(level, arg) { @@ -697,20 +700,20 @@ > let levelArg; > let logArg; > kbnHandlebarsEnv!.log = function (level, arg) { -605,606c507,508 +605,606c509,510 < equals('03', levelArg); < equals('whee', logArg); --- > expect('03').toEqual(levelArg); > expect('whee').toEqual(logArg); -609,610c511,513 +609,610c513,515 < it('should output to info', function() { < var called; --- > it('should output to info', function () { > let calls = 0; > const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; -612,616c515,521 +612,616c517,523 < console.info = function(info) { < equals('whee', info); < called = true; @@ -724,7 +727,7 @@ > console.info = $info; > console.log = $log; > } -618,622c523,529 +618,622c525,531 < console.log = function(log) { < equals('whee', log); < called = true; @@ -738,7 +741,7 @@ > console.info = $info; > console.log = $log; > } -625,628c532,533 +625,628c534,535 < expectTemplate('{{log blah}}') < .withInput({ blah: 'whee' }) < .toCompileTo(''); @@ -746,14 +749,14 @@ --- > expectTemplate('{{log blah}}').withInput({ blah: 'whee' }).toCompileTo(''); > expect(calls).toEqual(callsExpected); -631,632c536,538 +631,632c538,540 < it('should log at data level', function() { < var called; --- > it('should log at data level', function () { > let calls = 0; > const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; -634,637c540,543 +634,637c542,545 < console.error = function(log) { < equals('whee', log); < called = true; @@ -763,20 +766,20 @@ > expect('whee').toEqual(log); > calls++; > if (calls === callsExpected) console.error = $error; -645c551 +645c553 < equals(true, called); --- > expect(calls).toEqual(callsExpected); -648,649c554,556 +648,649c556,558 < it('should handle missing logger', function() { < var called = false; --- > it('should handle missing logger', function () { > let calls = 0; > const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; -650a558 +650a560 > // @ts-expect-error -652,655c560,563 +652,655c562,565 < console.log = function(log) { < equals('whee', log); < called = true; @@ -786,18 +789,18 @@ > expect('whee').toEqual(log); > calls++; > if (calls === callsExpected) console.log = $log; -663c571 +663c573 < equals(true, called); --- > expect(calls).toEqual(callsExpected); -666,667c574,576 +666,667c576,578 < it('should handle string log levels', function() { < var called; --- > it('should handle string log levels', function () { > let calls = 0; > const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; -669,671c578,580 +669,671c580,582 < console.error = function(log) { < equals('whee', log); < called = true; @@ -805,26 +808,26 @@ > console.error = function (log) { > expect('whee').toEqual(log); > calls++; -679c588 +679c590 < equals(true, called); --- > expect(calls).toEqual(callsExpected); -681c590 +681c592 < called = false; --- > calls = 0; -688c597 +688c599 < equals(true, called); --- > expect(calls).toEqual(callsExpected); -691,692c600,602 +691,692c602,604 < it('should handle hash log levels', function() { < var called; --- > it('should handle hash log levels [1]', function () { > let calls = 0; > const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; -694,696c604,606 +694,696c606,608 < console.error = function(log) { < equals('whee', log); < called = true; @@ -832,7 +835,7 @@ > console.error = function (log) { > expect('whee').toEqual(log); > calls++; -699,702c609,610 +699,702c611,612 < expectTemplate('{{log blah level="error"}}') < .withInput({ blah: 'whee' }) < .toCompileTo(''); @@ -840,13 +843,13 @@ --- > expectTemplate('{{log blah level="error"}}').withInput({ blah: 'whee' }).toCompileTo(''); > expect(calls).toEqual(callsExpected); -705,706c613,614 +705,706c615,616 < it('should handle hash log levels', function() { < var called = false; --- > it('should handle hash log levels [2]', function () { > let called = false; -708,711c616,623 +708,711c618,625 < console.info = console.log = console.error = console.debug = function() { < called = true; < console.info = console.log = console.error = console.debug = $log; @@ -860,7 +863,7 @@ > called = true; > console.info = console.log = console.error = console.debug = $log; > }; -713,716c625,626 +713,716c627,628 < expectTemplate('{{log blah level="debug"}}') < .withInput({ blah: 'whee' }) < .toCompileTo(''); @@ -868,14 +871,14 @@ --- > expectTemplate('{{log blah level="debug"}}').withInput({ blah: 'whee' }).toCompileTo(''); > expect(false).toEqual(called); -719,720c629,631 +719,720c631,633 < it('should pass multiple log arguments', function() { < var called; --- > it('should pass multiple log arguments', function () { > let calls = 0; > const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; -722,727c633,638 +722,727c635,640 < console.info = console.log = function(log1, log2, log3) { < equals('whee', log1); < equals('foo', log2); @@ -889,7 +892,7 @@ > expect(1).toEqual(log3); > calls++; > if (calls === callsExpected) console.log = $log; -730,733c641,642 +730,733c643,644 < expectTemplate('{{log blah "foo" 1}}') < .withInput({ blah: 'whee' }) < .toCompileTo(''); @@ -897,14 +900,14 @@ --- > expectTemplate('{{log blah "foo" 1}}').withInput({ blah: 'whee' }).toCompileTo(''); > expect(calls).toEqual(callsExpected); -736,737c645,647 +736,737c647,649 < it('should pass zero log arguments', function() { < var called; --- > it('should pass zero log arguments', function () { > let calls = 0; > const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; -739,742c649,652 +739,742c651,654 < console.info = console.log = function() { < expect(arguments.length).to.equal(0); < called = true; @@ -914,7 +917,7 @@ > expect(arguments.length).toEqual(0); > calls++; > if (calls === callsExpected) console.log = $log; -745,748c655,656 +745,748c657,658 < expectTemplate('{{log}}') < .withInput({ blah: 'whee' }) < .toCompileTo(''); @@ -922,13 +925,13 @@ --- > expectTemplate('{{log}}').withInput({ blah: 'whee' }).toCompileTo(''); > expect(calls).toEqual(callsExpected); -753,754c661,662 +753,754c663,664 < describe('#lookup', function() { < it('should lookup arbitrary content', function() { --- > describe('#lookup', () => { > it('should lookup arbitrary content', () => { -760c668 +760c670 < it('should not fail on undefined value', function() { --- > it('should not fail on undefined value', () => { diff --git a/packages/kbn-handlebars/.patches/compiler.patch b/packages/kbn-handlebars/.patches/compiler.patch index f1ba8aaa2b8b6..8b0a2672d0b03 100644 --- a/packages/kbn-handlebars/.patches/compiler.patch +++ b/packages/kbn-handlebars/.patches/compiler.patch @@ -1,8 +1,14 @@ -1,4c1,6 +1,10c1,6 < describe('compiler', function() { < if (!Handlebars.compile) { < return; < } +< +< describe('#equals', function() { +< function compile(string) { +< var ast = Handlebars.parse(string); +< return new Handlebars.Compiler().compile(ast, {}); +< } --- > /* > * This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js), @@ -10,15 +16,7 @@ > * Elasticsearch B.V. licenses this file to you under the MIT License. > * See `packages/kbn-handlebars/LICENSE` for more information. > */ -6,10c8 -< describe('#equals', function() { -< function compile(string) { -< var ast = Handlebars.parse(string); -< return new Handlebars.Compiler().compile(ast, {}); -< } ---- -> import Handlebars from '../..'; -12,60c10,13 +12,60c8,9 < it('should treat as equal', function() { < equal(compile('foo').equals(compile('foo')), true); < equal(compile('{{foo}}').equals(compile('{{foo}}')), true); @@ -69,11 +67,9 @@ < }); < }); --- -> describe('compiler', () => { -> const compileFns = ['compile', 'compileAST']; -> if (process.env.AST) compileFns.splice(0, 1); -> else if (process.env.EVAL) compileFns.splice(1, 1); -62,78c15,17 +> import Handlebars from '../..'; +> import { forEachCompileFunctionName } from '../__jest__/test_bench'; +62,78c11,13 < describe('#compile', function() { < it('should fail with invalid input', function() { < shouldThrow( @@ -92,10 +88,10 @@ < ); < }); --- -> compileFns.forEach((compileName) => { -> // @ts-expect-error -> const compile = Handlebars[compileName]; -80,92c19,24 +> describe('compiler', () => { +> forEachCompileFunctionName((compileName) => { +> const compile = Handlebars[compileName].bind(Handlebars); +80,92c15,20 < it('should include the location in the error (row and column)', function() { < try { < Handlebars.compile(' \n {{#if}}\n{{/def}}')(); @@ -116,7 +112,7 @@ > compile(null); > }).toThrow( > `You must pass a string or Handlebars AST to Handlebars.${compileName}. You passed null` -94,102d25 +94,102d21 < if (Object.getOwnPropertyDescriptor(err, 'column').writable) { < // In Safari 8, the column-property is read-only. This means that even if it is set with defineProperty, < // its value won't change (https://github.com/jquery/esprima/issues/1290#issuecomment-132455482) @@ -126,7 +122,7 @@ < equal(err.lineNumber, 2, 'Checking error row'); < } < }); -104,116c27,30 +104,116c23,26 < it('should include the location as enumerable property', function() { < try { < Handlebars.compile(' \n {{#if}}\n{{/def}}')(); @@ -145,7 +141,7 @@ > compile({}); > }).toThrow( > `You must pass a string or Handlebars AST to Handlebars.${compileName}. You passed [object Object]` -118,129c32 +118,129c28 < } < }); < @@ -160,7 +156,7 @@ < }); --- > }); -131,133c34,48 +131,133c30,44 < it('can pass through an empty string', function() { < equal(Handlebars.compile('')(), ''); < }); @@ -180,7 +176,7 @@ > expect(err.lineNumber).toEqual(2); > } > }); -135,142c50,57 +135,142c46,53 < it('should not modify the options.data property(GH-1327)', function() { < var options = { data: [{ a: 'foo' }, { a: 'bar' }] }; < Handlebars.compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)(); @@ -198,7 +194,7 @@ > expect(Object.prototype.propertyIsEnumerable.call(err, 'column')).toEqual(true); > } > }); -144,152c59,66 +144,152c55,62 < it('should not modify the options.knownHelpers property(GH-1327)', function() { < var options = { knownHelpers: {} }; < Handlebars.compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)(); @@ -217,7 +213,7 @@ > })({}) > ).toEqual('Hello'); > }); -154,170c68,70 +154,170c64,66 < describe('#precompile', function() { < it('should fail with invalid input', function() { < shouldThrow( @@ -239,7 +235,7 @@ > it('can pass through an empty string', () => { > expect(compile('')({})).toEqual(''); > }); -172,182c72,78 +172,182c68,75 < it('can utilize AST instance', function() { < equal( < /return "Hello"/.test( @@ -253,13 +249,14 @@ < }); --- > it('should not modify the options.data property(GH-1327)', () => { -> const options = { data: [{ a: 'foo' }, { a: 'bar' }] }; +> // The `data` property is supposed to be a boolean, but in this test we want to ignore that +> const options = { data: [{ a: 'foo' }, { a: 'bar' }] as unknown as boolean }; > compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)({}); > expect(JSON.stringify(options, null, 2)).toEqual( > JSON.stringify({ data: [{ a: 'foo' }, { a: 'bar' }] }, null, 2) > ); > }); -184,185c80,86 +184,185c77,83 < it('can pass through an empty string', function() { < equal(/return ""/.test(Handlebars.precompile('')), true); --- diff --git a/packages/kbn-handlebars/.patches/data.patch b/packages/kbn-handlebars/.patches/data.patch index ce1be00c8deaf..037214dddc3a1 100644 --- a/packages/kbn-handlebars/.patches/data.patch +++ b/packages/kbn-handlebars/.patches/data.patch @@ -45,47 +45,50 @@ > for (const prop in options.hash) { 40d48 < .withMessage('Automatic data was triggered') -44c52 +41a50,51 +> +> global.kbnHandlebarsEnv = null; +44c54 < it('parameter data can be looked up via @foo', function() { --- > it('parameter data can be looked up via @foo', () => { -47c55 +47c57 < .withHelper('hello', function(noun) { --- > .withHelper('hello', function (noun) { -50d57 +50d59 < .withMessage('@foo as a parameter retrieves template data') -54c61 +54c63 < it('hash values can be looked up via @foo', function() { --- > it('hash values can be looked up via @foo', () => { -57c64 +57c66 < .withHelper('hello', function(options) { --- > .withHelper('hello', function (options) { -60d66 +60d68 < .withMessage('@foo as a parameter retrieves template data') -64c70 +64c72 < it('nested parameter data can be looked up via @foo.bar', function() { --- > it('nested parameter data can be looked up via @foo.bar', () => { -67c73 +67c75 < .withHelper('hello', function(noun) { --- > .withHelper('hello', function (noun) { -70d75 +70d77 < .withMessage('@foo as a parameter retrieves template data') -74c79 +74c81 < it('nested parameter data does not fail with @world.bar', function() { --- > it('nested parameter data does not fail with @world.bar', () => { -77c82 +77c84 < .withHelper('hello', function(noun) { --- > .withHelper('hello', function (noun) { -80d84 +80d86 < .withMessage('@foo as a parameter retrieves template data') -84,87c88,89 +84,87c90,91 < it('parameter data throws when using complex scope references', function() { < expectTemplate( < '{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}' @@ -93,39 +96,39 @@ --- > it('parameter data throws when using complex scope references', () => { > expectTemplate('{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}').toThrow(Error); -90c92 +90c94 < it('data can be functions', function() { --- > it('data can be functions', () => { -94c96 +94c98 < hello: function() { --- > hello() { -96,97c98,99 +96,97c100,101 < } < } --- > }, > }, -102c104 +102c106 < it('data can be functions with params', function() { --- > it('data can be functions with params', () => { -106c108 +106c110 < hello: function(arg) { --- > hello(arg: any) { -108,109c110,111 +108,109c112,113 < } < } --- > }, > }, -114c116 +114c118 < it('data is inherited downstream', function() { --- > it('data is inherited downstream', () => { -120,122c122,124 +120,122c124,126 < .withHelper('let', function(options) { < var frame = Handlebars.createFrame(options.data); < for (var prop in options.hash) { @@ -133,9 +136,9 @@ > .withHelper('let', function (this: any, options) { > const frame = Handlebars.createFrame(options.data); > for (const prop in options.hash) { -130d131 +130d133 < .withMessage('data variables are inherited downstream') -134,147c135 +134,147c137 < it('passing in data to a compiled function that expects data - works with helpers in partials', function() { < expectTemplate('{{>myPartial}}') < .withCompileOptions({ data: true }) @@ -152,63 +155,63 @@ < it('passing in data to a compiled function that expects data - works with helpers and parameters', function() { --- > it('passing in data to a compiled function that expects data - works with helpers and parameters', () => { -150c138 +150c140 < .withHelper('hello', function(noun, options) { --- > .withHelper('hello', function (this: any, noun, options) { -155d142 +155d144 < .withMessage('Data output by helper') -159c146 +159c148 < it('passing in data to a compiled function that expects data - works with block helpers', function() { --- > it('passing in data to a compiled function that expects data - works with block helpers', () => { -162c149 +162c151 < data: true --- > data: true, -164c151 +164c153 < .withHelper('hello', function(options) { --- > .withHelper('hello', function (this: any, options) { -167c154 +167c156 < .withHelper('world', function(options) { --- > .withHelper('world', function (this: any, options) { -172d158 +172d160 < .withMessage('Data output by helper') -176c162 +176c164 < it('passing in data to a compiled function that expects data - works with block helpers that use ..', function() { --- > it('passing in data to a compiled function that expects data - works with block helpers that use ..', () => { -179c165 +179c167 < .withHelper('hello', function(options) { --- > .withHelper('hello', function (options) { -182c168 +182c170 < .withHelper('world', function(thing, options) { --- > .withHelper('world', function (this: any, thing, options) { -187d172 +187d174 < .withMessage('Data output by helper') -191c176 +191c178 < it('passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..', function() { --- > it('passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..', () => { -194c179 +194c181 < .withHelper('hello', function(options) { --- > .withHelper('hello', function (options) { -197c182 +197c184 < .withHelper('world', function(thing, options) { --- > .withHelper('world', function (this: any, thing, options) { -202d186 +202d188 < .withMessage('Data output by helper') -206c190 +206c192 < it('you can override inherited data when invoking a helper', function() { --- > it('you can override inherited data when invoking a helper', () => { -209,213c193,194 +209,213c195,196 < .withHelper('hello', function(options) { < return options.fn( < { exclaim: '?', zomg: 'world' }, @@ -217,55 +220,55 @@ --- > .withHelper('hello', function (options) { > return options.fn({ exclaim: '?', zomg: 'world' }, { data: { adjective: 'sad' } }); -215c196 +215c198 < .withHelper('world', function(thing, options) { --- > .withHelper('world', function (this: any, thing, options) { -220d200 +220d202 < .withMessage('Overriden data output by helper') -224c204 +224c206 < it('you can override inherited data when invoking a helper with depth', function() { --- > it('you can override inherited data when invoking a helper with depth', () => { -227c207 +227c209 < .withHelper('hello', function(options) { --- > .withHelper('hello', function (options) { -230c210 +230c212 < .withHelper('world', function(thing, options) { --- > .withHelper('world', function (this: any, thing, options) { -235d214 +235d216 < .withMessage('Overriden data output by helper') -239,240c218,219 +239,240c220,221 < describe('@root', function() { < it('the root context can be looked up via @root', function() { --- > describe('@root', () => { > it('the root context can be looked up via @root', () => { -246,248c225 +246,248c227 < expectTemplate('{{@root.foo}}') < .withInput({ foo: 'hello' }) < .toCompileTo('hello'); --- > expectTemplate('{{@root.foo}}').withInput({ foo: 'hello' }).toCompileTo('hello'); -251c228 +251c230 < it('passed root values take priority', function() { --- > it('passed root values take priority', () => { -259,260c236,237 +259,260c238,239 < describe('nesting', function() { < it('the root context can be looked up via @root', function() { --- > describe('nesting', () => { > it('the root context can be looked up via @root', () => { -265,266c242,243 +265,266c244,245 < .withHelper('helper', function(options) { < var frame = Handlebars.createFrame(options.data); --- > .withHelper('helper', function (this: any, options) { > const frame = Handlebars.createFrame(options.data); -272,273c249,250 +272,273c251,252 < depth: 0 < } --- diff --git a/packages/kbn-handlebars/.patches/helpers.patch b/packages/kbn-handlebars/.patches/helpers.patch index c274bb4a69271..a98e1ffea1937 100644 --- a/packages/kbn-handlebars/.patches/helpers.patch +++ b/packages/kbn-handlebars/.patches/helpers.patch @@ -1,4 +1,4 @@ -1,2c1,16 +1,2c1,20 < describe('helpers', function() { < it('helper with complex lookup$', function() { --- @@ -16,13 +16,17 @@ > global.kbnHandlebarsEnv = Handlebars.create(); > }); > +> afterEach(() => { +> global.kbnHandlebarsEnv = null; +> }); +> > describe('helpers', () => { > it('helper with complex lookup$', () => { -6c20 +6c24 < goodbyes: [{ text: 'Goodbye', url: 'goodbye' }] --- > goodbyes: [{ text: 'Goodbye', url: 'goodbye' }], -8,11c22,23 +8,11c26,27 < .withHelper('link', function(prefix) { < return ( < '' + this.text + '' @@ -30,66 +34,66 @@ --- > .withHelper('link', function (this: any, prefix) { > return '' + this.text + ''; -16c28 +16c32 < it('helper for raw block gets raw content', function() { --- > it('helper for raw block gets raw content', () => { -19c31 +19c35 < .withHelper('raw', function(options) { --- > .withHelper('raw', function (options) { -22d33 +22d37 < .withMessage('raw block helper gets raw content') -26c37 +26c41 < it('helper for raw block gets parameters', function() { --- > it('helper for raw block gets parameters', () => { -29,30c40,42 +29,30c44,46 < .withHelper('raw', function(a, b, c, options) { < return options.fn() + a + b + c; --- > .withHelper('raw', function (a, b, c, options) { > const ret = options.fn() + a + b + c; > return ret; -32d43 +32d47 < .withMessage('raw block helper gets raw content') -36,37c47,48 +36,37c51,52 < describe('raw block parsing (with identity helper-function)', function() { < function runWithIdentityHelper(template, expected) { --- > describe('raw block parsing (with identity helper-function)', () => { > function runWithIdentityHelper(template: string, expected: string) { -39c50 +39c54 < .withHelper('identity', function(options) { --- > .withHelper('identity', function (options) { -45c56 +45c60 < it('helper for nested raw block gets raw content', function() { --- > it('helper for nested raw block gets raw content', () => { -52c63 +52c67 < it('helper for nested raw block works with empty content', function() { --- > it('helper for nested raw block works with empty content', () => { -56c67 +56c71 < xit('helper for nested raw block works if nested raw blocks are broken', function() { --- > it.skip('helper for nested raw block works if nested raw blocks are broken', () => { -67c78 +67c82 < it('helper for nested raw block closes after first matching close', function() { --- > it('helper for nested raw block closes after first matching close', () => { -74,75c85,86 +74,75c89,90 < it('helper for nested raw block throw exception when with missing closing braces', function() { < var string = '{{{{a}}}} {{{{/a'; --- > it('helper for nested raw block throw exception when with missing closing braces', () => { > const string = '{{{{a}}}} {{{{/a'; -80c91 +80c95 < it('helper block with identical context', function() { --- > it('helper block with identical context', () => { -83,86c94,97 +83,86c98,101 < .withHelper('goodbyes', function(options) { < var out = ''; < var byes = ['Goodbye', 'goodbye', 'GOODBYE']; @@ -99,11 +103,11 @@ > let out = ''; > const byes = ['Goodbye', 'goodbye', 'GOODBYE']; > for (let i = 0, j = byes.length; i < j; i++) { -94c105 +94c109 < it('helper block with complex lookup expression', function() { --- > it('helper block with complex lookup expression', () => { -97,100c108,111 +97,100c112,115 < .withHelper('goodbyes', function(options) { < var out = ''; < var byes = ['Goodbye', 'goodbye', 'GOODBYE']; @@ -113,7 +117,7 @@ > let out = ''; > const byes = ['Goodbye', 'goodbye', 'GOODBYE']; > for (let i = 0, j = byes.length; i < j; i++) { -108,111c119,120 +108,111c123,124 < it('helper with complex lookup and nested template', function() { < expectTemplate( < '{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}' @@ -121,11 +125,11 @@ --- > it('helper with complex lookup and nested template', () => { > expectTemplate('{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}') -114c123 +114c127 < goodbyes: [{ text: 'Goodbye', url: 'goodbye' }] --- > goodbyes: [{ text: 'Goodbye', url: 'goodbye' }], -116,125c125,126 +116,125c129,130 < .withHelper('link', function(prefix, options) { < return ( < '' + options.fn(this) + ''; -130,133c131,132 +130,133c135,136 < it('helper with complex lookup and nested template in VM+Compiler', function() { < expectTemplate( < '{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}' @@ -147,11 +151,11 @@ --- > it('helper with complex lookup and nested template in VM+Compiler', () => { > expectTemplate('{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}') -136c135 +136c139 < goodbyes: [{ text: 'Goodbye', url: 'goodbye' }] --- > goodbyes: [{ text: 'Goodbye', url: 'goodbye' }], -138,147c137,138 +138,147c141,142 < .withHelper('link', function(prefix, options) { < return ( < '' + options.fn(this) + ''; -152c143 +152c147 < it('helper returning undefined value', function() { --- > it('helper returning undefined value', () => { -155c146 +155c150 < nothere: function() {} --- > nothere() {}, -161c152 +161c156 < nothere: function() {} --- > nothere() {}, -166c157 +166c161 < it('block helper', function() { --- > it('block helper', () => { -169c160 +169c164 < .withHelper('goodbyes', function(options) { --- > .withHelper('goodbyes', function (options) { -172d162 +172d166 < .withMessage('Block helper executed') -176c166 +176c170 < it('block helper staying in the same context', function() { --- > it('block helper staying in the same context', () => { -179c169 +179c173 < .withHelper('form', function(options) { --- > .withHelper('form', function (this: any, options) { -182d171 +182d175 < .withMessage('Block helper executed with current context') -186,187c175,176 +186,187c179,180 < it('block helper should have context in this', function() { < function link(options) { --- > it('block helper should have context in this', () => { > function link(this: any, options: Handlebars.HelperOptions) { -191,193c180 +191,193c184 < expectTemplate( < '
    {{#people}}
  • {{#link}}{{name}}{{/link}}
  • {{/people}}
' < ) --- > expectTemplate('
    {{#people}}
  • {{#link}}{{name}}{{/link}}
  • {{/people}}
') -197,198c184,185 +197,198c188,189 < { name: 'Yehuda', id: 2 } < ] --- > { name: 'Yehuda', id: 2 }, > ], -206c193 +206c197 < it('block helper for undefined value', function() { --- > it('block helper for undefined value', () => { -210c197 +210c201 < it('block helper passing a new context', function() { --- > it('block helper passing a new context', () => { -213c200 +213c204 < .withHelper('form', function(context, options) { --- > .withHelper('form', function (context, options) { -216d202 +216d206 < .withMessage('Context variable resolved') -220c206 +220c210 < it('block helper passing a complex path context', function() { --- > it('block helper passing a complex path context', () => { -223c209 +223c213 < .withHelper('form', function(context, options) { --- > .withHelper('form', function (context, options) { -226d211 +226d215 < .withMessage('Complex path variable resolved') -230,233c215,216 +230,233c219,220 < it('nested block helpers', function() { < expectTemplate( < '{{#form yehuda}}

{{name}}

{{#link}}Hello{{/link}}{{/form}}' @@ -247,21 +251,21 @@ --- > it('nested block helpers', () => { > expectTemplate('{{#form yehuda}}

{{name}}

{{#link}}Hello{{/link}}{{/form}}') -235c218 +235c222 < yehuda: { name: 'Yehuda' } --- > yehuda: { name: 'Yehuda' }, -237c220 +237c224 < .withHelper('link', function(options) { --- > .withHelper('link', function (this: any, options) { -240c223 +240c227 < .withHelper('form', function(context, options) { --- > .withHelper('form', function (context, options) { -243d225 +243d229 < .withMessage('Both blocks executed') -247,249c229,231 +247,249c233,235 < it('block helper inverted sections', function() { < var string = "{{#list people}}{{name}}{{^}}Nobody's here{{/list}}"; < function list(context, options) { @@ -269,32 +273,32 @@ > it('block helper inverted sections', () => { > const string = "{{#list people}}{{name}}{{^}}Nobody's here{{/list}}"; > function list(this: any, context: any, options: Handlebars.HelperOptions) { -251,252c233,234 +251,252c237,238 < var out = '
    '; < for (var i = 0, j = context.length; i < j; i++) { --- > let out = '
      '; > for (let i = 0, j = context.length; i < j; i++) { -268,269c250 +268,269c254 < .withHelpers({ list: list }) < .withMessage('an inverse wrapper is passed in as a new context') --- > .withHelpers({ list }) -274,275c255 +274,275c259 < .withHelpers({ list: list }) < .withMessage('an inverse wrapper can be optionally called') --- > .withHelpers({ list }) -281c261 +281c265 < message: "Nobody's here" --- > message: "Nobody's here", -283,284c263 +283,284c267 < .withHelpers({ list: list }) < .withMessage('the context of an inverse is the parent of the block') --- > .withHelpers({ list }) -288,292c267,269 +288,292c271,273 < it('pathed lambas with parameters', function() { < var hash = { < helper: function() { @@ -304,15 +308,15 @@ > it('pathed lambas with parameters', () => { > const hash = { > helper: () => 'winning', -293a271 +293a275 > // @ts-expect-error -295,299d272 +295,299d276 < var helpers = { < './helper': function() { < return 'fail'; < } < }; -301,304c274,276 +301,304c278,280 < expectTemplate('{{./helper 1}}') < .withInput(hash) < .withHelpers(helpers) @@ -321,7 +325,7 @@ > const helpers = { > './helper': () => 'fail', > }; -306,309c278,279 +306,309c282,283 < expectTemplate('{{hash/helper 1}}') < .withInput(hash) < .withHelpers(helpers) @@ -329,91 +333,91 @@ --- > expectTemplate('{{./helper 1}}').withInput(hash).withHelpers(helpers).toCompileTo('winning'); > expectTemplate('{{hash/helper 1}}').withInput(hash).withHelpers(helpers).toCompileTo('winning'); -312,313c282,283 +312,313c286,287 < describe('helpers hash', function() { < it('providing a helpers hash', function() { --- > describe('helpers hash', () => { > it('providing a helpers hash', () => { -317c287 +317c291 < world: function() { --- > world() { -319c289 +319c293 < } --- > }, -321d290 +321d294 < .withMessage('helpers hash is available') -327c296 +327c300 < world: function() { --- > world() { -329c298 +329c302 < } --- > }, -331d299 +331d303 < .withMessage('helpers hash is available inside other blocks') -335c303 +335c307 < it('in cases of conflict, helpers win', function() { --- > it('in cases of conflict, helpers win', () => { -339c307 +339c311 < lookup: function() { --- > lookup() { -341c309 +341c313 < } --- > }, -343d310 +343d314 < .withMessage('helpers hash has precedence escaped expansion') -349c316 +349c320 < lookup: function() { --- > lookup() { -351c318 +351c322 < } --- > }, -353d319 +353d323 < .withMessage('helpers hash has precedence simple expansion') -357c323 +357c327 < it('the helpers hash is available is nested contexts', function() { --- > it('the helpers hash is available is nested contexts', () => { -361c327 +361c331 < helper: function() { --- > helper() { -363c329 +363c333 < } --- > }, -365d330 +365d334 < .withMessage('helpers hash is available in nested contexts.') -369,370c334,335 +369,370c338,339 < it('the helper hash should augment the global hash', function() { < handlebarsEnv.registerHelper('test_helper', function() { --- > it('the helper hash should augment the global hash', () => { > kbnHandlebarsEnv!.registerHelper('test_helper', function () { -374,376c339 +374,376c343 < expectTemplate( < '{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}' < ) --- > expectTemplate('{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}') -379c342 +379c346 < world: function() { --- > world() { -381c344 +381c348 < } --- > }, -387,389c350,352 +387,389c354,356 < describe('registration', function() { < it('unregisters', function() { < handlebarsEnv.helpers = {}; @@ -421,18 +425,18 @@ > describe('registration', () => { > it('unregisters', () => { > deleteAllKeys(kbnHandlebarsEnv!.helpers); -391c354 +391c358 < handlebarsEnv.registerHelper('foo', function() { --- > kbnHandlebarsEnv!.registerHelper('foo', function () { -394,395c357,359 +394,395c361,363 < handlebarsEnv.unregisterHelper('foo'); < equals(handlebarsEnv.helpers.foo, undefined); --- > expect(kbnHandlebarsEnv!.helpers.foo).toBeDefined(); > kbnHandlebarsEnv!.unregisterHelper('foo'); > expect(kbnHandlebarsEnv!.helpers.foo).toBeUndefined(); -398,400c362,364 +398,400c366,368 < it('allows multiple globals', function() { < var helpers = handlebarsEnv.helpers; < handlebarsEnv.helpers = {}; @@ -440,7 +444,7 @@ > it('allows multiple globals', () => { > const ifHelper = kbnHandlebarsEnv!.helpers.if; > deleteAllKeys(kbnHandlebarsEnv!.helpers); -402,404c366,368 +402,404c370,372 < handlebarsEnv.registerHelper({ < if: helpers['if'], < world: function() { @@ -448,21 +452,21 @@ > kbnHandlebarsEnv!.registerHelper({ > if: ifHelper, > world() { -407c371 +407c375 < testHelper: function() { --- > testHelper() { -409c373 +409c377 < } --- > }, -412,414c376 +412,414c380 < expectTemplate( < '{{testHelper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}' < ) --- > expectTemplate('{{testHelper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}') -419,429c381,387 +419,429c385,391 < it('fails with multiple and args', function() { < shouldThrow( < function() { @@ -482,7 +486,7 @@ > { > world() { > return 'world!'; -431,436c389,395 +431,436c393,399 < {} < ); < }, @@ -497,69 +501,69 @@ > {} > ); > }).toThrow('Arg not supported with multiple helpers'); -440c399 +440c403 < it('decimal number literals work', function() { --- > it('decimal number literals work', () => { -442c401 +442c405 < .withHelper('hello', function(times, times2) { --- > .withHelper('hello', function (times, times2) { -451d409 +451d413 < .withMessage('template with a negative integer literal') -455c413 +455c417 < it('negative number literals work', function() { --- > it('negative number literals work', () => { -457c415 +457c419 < .withHelper('hello', function(times) { --- > .withHelper('hello', function (times) { -463d420 +463d424 < .withMessage('template with a negative integer literal') -467,468c424,425 +467,468c428,429 < describe('String literal parameters', function() { < it('simple literals work', function() { --- > describe('String literal parameters', () => { > it('simple literals work', () => { -470c427 +470c431 < .withHelper('hello', function(param, times, bool1, bool2) { --- > .withHelper('hello', function (param, times, bool1, bool2) { -480,482c437 +480,482c441 < return ( < 'Hello ' + param + ' ' + times + ' times: ' + bool1 + ' ' + bool2 < ); --- > return 'Hello ' + param + ' ' + times + ' times: ' + bool1 + ' ' + bool2; -484d438 +484d442 < .withMessage('template with a simple String literal') -488c442 +488c446 < it('using a quote in the middle of a parameter raises an error', function() { --- > it('using a quote in the middle of a parameter raises an error', () => { -492c446 +492c450 < it('escaping a String is possible', function() { --- > it('escaping a String is possible', () => { -494c448 +494c452 < .withHelper('hello', function(param) { --- > .withHelper('hello', function (param) { -497d450 +497d454 < .withMessage('template with an escaped String literal') -501c454 +501c458 < it("it works with ' marks", function() { --- > it("it works with ' marks", () => { -503c456 +503c460 < .withHelper('hello', function(param) { --- > .withHelper('hello', function (param) { -506d458 +506d462 < .withMessage("template with a ' mark") -511,524c463,464 +511,524c467,468 < it('negative number literals work', function() { < expectTemplate('Message: {{hello -12}}') < .withHelper('hello', function(times) { @@ -577,13 +581,13 @@ --- > describe('multiple parameters', () => { > it('simple multi-params work', () => { -527c467 +527c471 < .withHelper('goodbye', function(cruel, world) { --- > .withHelper('goodbye', function (cruel, world) { -530d469 +530d473 < .withMessage('regular helpers with multiple params') -534,537c473,474 +534,537c477,478 < it('block multi-params work', function() { < expectTemplate( < 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}' @@ -591,65 +595,65 @@ --- > it('block multi-params work', () => { > expectTemplate('Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}') -539c476 +539c480 < .withHelper('goodbye', function(cruel, world, options) { --- > .withHelper('goodbye', function (cruel, world, options) { -542d478 +542d482 < .withMessage('block helpers with multiple params') -547,548c483,484 +547,548c487,488 < describe('hash', function() { < it('helpers can take an optional hash', function() { --- > describe('hash', () => { > it('helpers can take an optional hash', () => { -550c486 +550c490 < .withHelper('goodbye', function(options) { --- > .withHelper('goodbye', function (options) { -561d496 +561d500 < .withMessage('Helper output hash') -565,566c500,501 +565,566c504,505 < it('helpers can take an optional hash with booleans', function() { < function goodbye(options) { --- > it('helpers can take an optional hash with booleans', () => { > function goodbye(options: Handlebars.HelperOptions) { -578d512 +578d516 < .withMessage('Helper output hash') -583d516 +583d520 < .withMessage('Boolean helper parameter honored') -587c520 +587c524 < it('block helpers can take an optional hash', function() { --- > it('block helpers can take an optional hash', () => { -589c522 +589c526 < .withHelper('goodbye', function(options) { --- > .withHelper('goodbye', function (this: any, options) { -600d532 +600d536 < .withMessage('Hash parameters output') -604c536 +604c540 < it('block helpers can take an optional hash with single quoted stings', function() { --- > it('block helpers can take an optional hash with single quoted stings', () => { -606c538 +606c542 < .withHelper('goodbye', function(options) { --- > .withHelper('goodbye', function (this: any, options) { -617d548 +617d552 < .withMessage('Hash parameters output') -621,622c552,553 +621,622c556,557 < it('block helpers can take an optional hash with booleans', function() { < function goodbye(options) { --- > it('block helpers can take an optional hash with booleans', () => { > function goodbye(this: any, options: Handlebars.HelperOptions) { -634d564 +634d568 < .withMessage('Boolean hash parameter honored') -639d568 +639d572 < .withMessage('Boolean hash parameter honored') -644,648c573,575 +644,648c577,579 < describe('helperMissing', function() { < it('if a context is not found, helperMissing is used', function() { < expectTemplate('{{hello}} {{link_to world}}').toThrow( @@ -659,135 +663,135 @@ > describe('helperMissing', () => { > it('if a context is not found, helperMissing is used', () => { > expectTemplate('{{hello}} {{link_to world}}').toThrow(/Missing helper: "link_to"/); -651c578 +651c582 < it('if a context is not found, custom helperMissing is used', function() { --- > it('if a context is not found, custom helperMissing is used', () => { -654c581 +654c585 < .withHelper('helperMissing', function(mesg, options) { --- > .withHelper('helperMissing', function (mesg, options) { -662c589 +662c593 < it('if a value is not found, custom helperMissing is used', function() { --- > it('if a value is not found, custom helperMissing is used', () => { -665c592 +665c596 < .withHelper('helperMissing', function(options) { --- > .withHelper('helperMissing', function (options) { -674,675c601,602 +674,675c605,606 < describe('knownHelpers', function() { < it('Known helper should render helper', function() { --- > describe('knownHelpers', () => { > it('Known helper should render helper', () => { -678c605 +678c609 < knownHelpers: { hello: true } --- > knownHelpers: { hello: true }, -680c607 +680c611 < .withHelper('hello', function() { --- > .withHelper('hello', function () { -686c613 +686c617 < it('Unknown helper in knownHelpers only mode should be passed as undefined', function() { --- > it('Unknown helper in knownHelpers only mode should be passed as undefined', () => { -690c617 +690c621 < knownHelpersOnly: true --- > knownHelpersOnly: true, -692c619 +692c623 < .withHelper('typeof', function(arg) { --- > .withHelper('typeof', function (arg) { -695c622 +695c626 < .withHelper('hello', function() { --- > .withHelper('hello', function () { -701c628 +701c632 < it('Builtin helpers available in knownHelpers only mode', function() { --- > it('Builtin helpers available in knownHelpers only mode', () => { -704c631 +704c635 < knownHelpersOnly: true --- > knownHelpersOnly: true, -709c636 +709c640 < it('Field lookup works in knownHelpers only mode', function() { --- > it('Field lookup works in knownHelpers only mode', () => { -712c639 +712c643 < knownHelpersOnly: true --- > knownHelpersOnly: true, -718c645 +718c649 < it('Conditional blocks work in knownHelpers only mode', function() { --- > it('Conditional blocks work in knownHelpers only mode', () => { -721c648 +721c652 < knownHelpersOnly: true --- > knownHelpersOnly: true, -727c654 +727c658 < it('Invert blocks work in knownHelpers only mode', function() { --- > it('Invert blocks work in knownHelpers only mode', () => { -730c657 +730c661 < knownHelpersOnly: true --- > knownHelpersOnly: true, -736c663 +736c667 < it('Functions are bound to the context in knownHelpers only mode', function() { --- > it('Functions are bound to the context in knownHelpers only mode', () => { -739c666 +739c670 < knownHelpersOnly: true --- > knownHelpersOnly: true, -742c669 +742c673 < foo: function() { --- > foo() { -745c672 +745c676 < bar: 'bar' --- > bar: 'bar', -750c677 +750c681 < it('Unknown helper call in knownHelpers only mode should throw', function() { --- > it('Unknown helper call in knownHelpers only mode should throw', () => { -757,758c684,685 +757,758c688,689 < describe('blockHelperMissing', function() { < it('lambdas are resolved by blockHelperMissing, not handlebars proper', function() { --- > describe('blockHelperMissing', () => { > it('lambdas are resolved by blockHelperMissing, not handlebars proper', () => { -761c688 +761c692 < truthy: function() { --- > truthy() { -763c690 +763c694 < } --- > }, -768c695 +768c699 < it('lambdas resolved by blockHelperMissing are bound to the context', function() { --- > it('lambdas resolved by blockHelperMissing are bound to the context', () => { -771c698 +771c702 < truthy: function() { --- > truthy() { -774c701 +774c705 < truthiness: function() { --- > truthiness() { -776c703 +776c707 < } --- > }, -782,785c709,712 +782,785c713,716 < describe('name field', function() { < var helpers = { < blockHelperMissing: function() { @@ -797,7 +801,7 @@ > const helpers = { > blockHelperMissing(...args: any[]) { > return 'missing: ' + args[args.length - 1].name; -787,788c714,718 +787,788c718,722 < helperMissing: function() { < return 'helper missing: ' + arguments[arguments.length - 1].name; --- @@ -806,11 +810,11 @@ > }, > helper(...args: any[]) { > return 'ran: ' + args[args.length - 1].name; -790,792d719 +790,792d723 < helper: function() { < return 'ran: ' + arguments[arguments.length - 1].name; < } -795,798c722,723 +795,798c726,727 < it('should include in ambiguous mustache calls', function() { < expectTemplate('{{helper}}') < .withHelpers(helpers) @@ -818,7 +822,7 @@ --- > it('should include in ambiguous mustache calls', () => { > expectTemplate('{{helper}}').withHelpers(helpers).toCompileTo('ran: helper'); -801,804c726,727 +801,804c730,731 < it('should include in helper mustache calls', function() { < expectTemplate('{{helper 1}}') < .withHelpers(helpers) @@ -826,7 +830,7 @@ --- > it('should include in helper mustache calls', () => { > expectTemplate('{{helper 1}}').withHelpers(helpers).toCompileTo('ran: helper'); -807,810c730,731 +807,810c734,735 < it('should include in ambiguous block calls', function() { < expectTemplate('{{#helper}}{{/helper}}') < .withHelpers(helpers) @@ -834,11 +838,11 @@ --- > it('should include in ambiguous block calls', () => { > expectTemplate('{{#helper}}{{/helper}}').withHelpers(helpers).toCompileTo('ran: helper'); -813c734 +813c738 < it('should include in simple block calls', function() { --- > it('should include in simple block calls', () => { -819,822c740,741 +819,822c744,745 < it('should include in helper block calls', function() { < expectTemplate('{{#helper 1}}{{/helper}}') < .withHelpers(helpers) @@ -846,79 +850,79 @@ --- > it('should include in helper block calls', () => { > expectTemplate('{{#helper 1}}{{/helper}}').withHelpers(helpers).toCompileTo('ran: helper'); -825c744 +825c748 < it('should include in known helper calls', function() { --- > it('should include in known helper calls', () => { -829c748 +829c752 < knownHelpersOnly: true --- > knownHelpersOnly: true, -835c754 +835c758 < it('should include full id', function() { --- > it('should include full id', () => { -842c761 +842c765 < it('should include full id if a hash is passed', function() { --- > it('should include full id if a hash is passed', () => { -850,851c769,770 +850,851c773,774 < describe('name conflicts', function() { < it('helpers take precedence over same-named context properties', function() { --- > describe('name conflicts', () => { > it('helpers take precedence over same-named context properties', () => { -853c772 +853c776 < .withHelper('goodbye', function() { --- > .withHelper('goodbye', function (this: any) { -856c775 +856c779 < .withHelper('cruel', function(world) { --- > .withHelper('cruel', function (world) { -861c780 +861c784 < world: 'world' --- > world: 'world', -863d781 +863d785 < .withMessage('Helper executed') -867c785 +867c789 < it('helpers take precedence over same-named context properties$', function() { --- > it('helpers take precedence over same-named context properties$', () => { -869c787 +869c791 < .withHelper('goodbye', function(options) { --- > .withHelper('goodbye', function (this: any, options) { -872c790 +872c794 < .withHelper('cruel', function(world) { --- > .withHelper('cruel', function (world) { -877c795 +877c799 < world: 'world' --- > world: 'world', -879d796 +879d800 < .withMessage('Helper executed') -883c800 +883c804 < it('Scoped names take precedence over helpers', function() { --- > it('Scoped names take precedence over helpers', () => { -885c802 +885c806 < .withHelper('goodbye', function() { --- > .withHelper('goodbye', function (this: any) { -888c805 +888c809 < .withHelper('cruel', function(world) { --- > .withHelper('cruel', function (world) { -893c810 +893c814 < world: 'world' --- > world: 'world', -895d811 +895d815 < .withMessage('Helper not executed') -899,903c815,817 +899,903c819,821 < it('Scoped names take precedence over block helpers', function() { < expectTemplate( < '{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}' @@ -928,43 +932,43 @@ > it('Scoped names take precedence over block helpers', () => { > expectTemplate('{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}') > .withHelper('goodbye', function (this: any, options) { -906c820 +906c824 < .withHelper('cruel', function(world) { --- > .withHelper('cruel', function (world) { -911c825 +911c829 < world: 'world' --- > world: 'world', -913d826 +913d830 < .withMessage('Helper executed') -918,919c831,832 +918,919c835,836 < describe('block params', function() { < it('should take presedence over context values', function() { --- > describe('block params', () => { > it('should take presedence over context values', () => { -922,923c835,836 +922,923c839,840 < .withHelper('goodbyes', function(options) { < equals(options.fn.blockParams, 1); --- > .withHelper('goodbyes', function (options) { > expect(options.fn.blockParams).toEqual(1); -929c842 +929c846 < it('should take presedence over helper values', function() { --- > it('should take presedence over helper values', () => { -931c844 +931c848 < .withHelper('value', function() { --- > .withHelper('value', function () { -934,935c847,848 +934,935c851,852 < .withHelper('goodbyes', function(options) { < equals(options.fn.blockParams, 1); --- > .withHelper('goodbyes', function (options) { > expect(options.fn.blockParams).toEqual(1); -941,944c854,855 +941,944c858,859 < it('should not take presedence over pathed values', function() { < expectTemplate( < '{{#goodbyes as |value|}}{{./value}}{{/goodbyes}}{{value}}' @@ -972,23 +976,23 @@ --- > it('should not take presedence over pathed values', () => { > expectTemplate('{{#goodbyes as |value|}}{{./value}}{{/goodbyes}}{{value}}') -946c857 +946c861 < .withHelper('value', function() { --- > .withHelper('value', function () { -949,950c860,861 +949,950c864,865 < .withHelper('goodbyes', function(options) { < equals(options.fn.blockParams, 1); --- > .withHelper('goodbyes', function (this: any, options) { > expect(options.fn.blockParams).toEqual(1); -956,957c867,868 +956,957c871,872 < it('should take presednece over parent block params', function() { < var value = 1; --- > it('should take presednece over parent block params', () => { > let value: number; -959c870,875 +959c874,879 < '{{#goodbyes as |value|}}{{#goodbyes}}{{value}}{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{/goodbyes}}{{/goodbyes}}{{value}}' --- > '{{#goodbyes as |value|}}{{#goodbyes}}{{value}}{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{/goodbyes}}{{/goodbyes}}{{value}}', @@ -997,16 +1001,16 @@ > value = 1; > }, > } -962c878 +962c882 < .withHelper('goodbyes', function(options) { --- > .withHelper('goodbyes', function (options) { -966,967c882 +966,967c886 < blockParams: < options.fn.blockParams === 1 ? [value++, value++] : undefined --- > blockParams: options.fn.blockParams === 1 ? [value++, value++] : undefined, -974,977c889,890 +974,977c893,894 < it('should allow block params on chained helpers', function() { < expectTemplate( < '{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}' @@ -1014,13 +1018,13 @@ --- > it('should allow block params on chained helpers', () => { > expectTemplate('{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}') -979,980c892,893 +979,980c896,897 < .withHelper('goodbyes', function(options) { < equals(options.fn.blockParams, 1); --- > .withHelper('goodbyes', function (options) { > expect(options.fn.blockParams).toEqual(1); -987,991c900,902 +987,991c904,906 < describe('built-in helpers malformed arguments ', function() { < it('if helper - too few arguments', function() { < expectTemplate('{{#if}}{{/if}}').toThrow( @@ -1030,7 +1034,7 @@ > describe('built-in helpers malformed arguments ', () => { > it('if helper - too few arguments', () => { > expectTemplate('{{#if}}{{/if}}').toThrow(/#if requires exactly one argument/); -994,997c905,906 +994,997c909,910 < it('if helper - too many arguments, string', function() { < expectTemplate('{{#if test "string"}}{{/if}}').toThrow( < /#if requires exactly one argument/ @@ -1038,7 +1042,7 @@ --- > it('if helper - too many arguments, string', () => { > expectTemplate('{{#if test "string"}}{{/if}}').toThrow(/#if requires exactly one argument/); -1000,1003c909,910 +1000,1003c913,914 < it('if helper - too many arguments, undefined', function() { < expectTemplate('{{#if test undefined}}{{/if}}').toThrow( < /#if requires exactly one argument/ @@ -1046,7 +1050,7 @@ --- > it('if helper - too many arguments, undefined', () => { > expectTemplate('{{#if test undefined}}{{/if}}').toThrow(/#if requires exactly one argument/); -1006,1009c913,914 +1006,1009c917,918 < it('if helper - too many arguments, null', function() { < expectTemplate('{{#if test null}}{{/if}}').toThrow( < /#if requires exactly one argument/ @@ -1054,7 +1058,7 @@ --- > it('if helper - too many arguments, null', () => { > expectTemplate('{{#if test null}}{{/if}}').toThrow(/#if requires exactly one argument/); -1012,1015c917,918 +1012,1015c921,922 < it('unless helper - too few arguments', function() { < expectTemplate('{{#unless}}{{/unless}}').toThrow( < /#unless requires exactly one argument/ @@ -1062,11 +1066,11 @@ --- > it('unless helper - too few arguments', () => { > expectTemplate('{{#unless}}{{/unless}}').toThrow(/#unless requires exactly one argument/); -1018c921 +1018c925 < it('unless helper - too many arguments', function() { --- > it('unless helper - too many arguments', () => { -1024,1027c927,928 +1024,1027c931,932 < it('with helper - too few arguments', function() { < expectTemplate('{{#with}}{{/with}}').toThrow( < /#with requires exactly one argument/ @@ -1074,21 +1078,21 @@ --- > it('with helper - too few arguments', () => { > expectTemplate('{{#with}}{{/with}}').toThrow(/#with requires exactly one argument/); -1030c931 +1030c935 < it('with helper - too many arguments', function() { --- > it('with helper - too many arguments', () => { -1037,1038c938,939 +1037,1038c942,943 < describe('the lookupProperty-option', function() { < it('should be passed to custom helpers', function() { --- > describe('the lookupProperty-option', () => { > it('should be passed to custom helpers', () => { -1040c941 +1040c945 < .withHelper('testHelper', function testHelper(options) { --- > .withHelper('testHelper', function testHelper(this: any, options) { -1047a949,954 +1047a953,958 > > function deleteAllKeys(obj: { [key: string]: any }) { > for (const key of Object.keys(obj)) { diff --git a/packages/kbn-handlebars/README.md b/packages/kbn-handlebars/README.md index a54d56a11d7dd..0f7ae9b0d4668 100644 --- a/packages/kbn-handlebars/README.md +++ b/packages/kbn-handlebars/README.md @@ -5,17 +5,18 @@ A custom version of the handlebars package which, to improve security, does not ## Limitations - Only the following compile options are supported: + - `data` - `knownHelpers` - `knownHelpersOnly` + - `noEscape` - `strict` - `assumeObjects` - - `noEscape` - - `data` - Only the following runtime options are supported: - - `helpers` - - `blockParams` - `data` + - `helpers` + - `decorators` (not documented in the official Handlebars [runtime options documentation](https://handlebarsjs.com/api-reference/runtime-options.html)) + - `blockParams` (not documented in the official Handlebars [runtime options documentation](https://handlebarsjs.com/api-reference/runtime-options.html)) The [Inline partials](https://handlebarsjs.com/guide/partials.html#inline-partials) handlebars template feature is currently not supported by `@kbn/handlebars`. diff --git a/packages/kbn-handlebars/__snapshots__/index.test.ts.snap b/packages/kbn-handlebars/__snapshots__/index.test.ts.snap index 9534cf6024e1f..b9a8c27e45911 100644 --- a/packages/kbn-handlebars/__snapshots__/index.test.ts.snap +++ b/packages/kbn-handlebars/__snapshots__/index.test.ts.snap @@ -1,19 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Handlebars.compileAST invalid template 1`] = ` -"Parse error on line 1: -{{value ---^ -Expecting 'ID', 'STRING', 'NUMBER', 'BOOLEAN', 'UNDEFINED', 'NULL', 'DATA', got 'INVALID'" -`; - -exports[`Handlebars.compileAST invalid template 2`] = ` -"Parse error on line 1: -{{value ---^ -Expecting 'ID', 'STRING', 'NUMBER', 'BOOLEAN', 'UNDEFINED', 'NULL', 'DATA', got 'INVALID'" -`; - exports[`Handlebars.create 1`] = ` HandlebarsEnvironment { "AST": Object { @@ -103,7 +89,3 @@ HandlebarsEnvironment { "template": [Function], } `; - -exports[`blocks decorators registration should not be able to call decorators unregistered using the \`unregisterDecorator\` function 1`] = `"lookupProperty(...) is not a function"`; - -exports[`blocks decorators registration should not be able to call decorators unregistered using the \`unregisterDecorator\` function 2`] = `"decoratorFn is not a function"`; diff --git a/packages/kbn-handlebars/index.test.ts b/packages/kbn-handlebars/index.test.ts index 6937e0582c265..9d255bf676746 100644 --- a/packages/kbn-handlebars/index.test.ts +++ b/packages/kbn-handlebars/index.test.ts @@ -11,7 +11,7 @@ */ import Handlebars from '.'; -import { expectTemplate } from './src/__jest__/test_bench'; +import { expectTemplate, forEachCompileFunctionName } from './src/__jest__/test_bench'; it('Handlebars.create', () => { expect(Handlebars.create()).toMatchSnapshot(); @@ -35,7 +35,10 @@ describe('Handlebars.compileAST', () => { }); it('invalid template', () => { - expectTemplate('{{value').withInput({ value: 42 }).toThrowErrorMatchingSnapshot(); + expectTemplate('{{value').withInput({ value: 42 }).toThrow(`Parse error on line 1: +{{value +--^ +Expecting 'ID', 'STRING', 'NUMBER', 'BOOLEAN', 'UNDEFINED', 'NULL', 'DATA', got 'INVALID'`); }); if (!process.env.EVAL) { @@ -108,33 +111,27 @@ describe('blocks', () => { expect(calls).toEqual(callsExpected); }); - it('should call decorator again if render function is called again', () => { - const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; + forEachCompileFunctionName((compileName) => { + it(`should call decorator again if render function is called again for #${compileName}`, () => { + global.kbnHandlebarsEnv = Handlebars.create(); - global.kbnHandlebarsEnv = Handlebars.create(); + kbnHandlebarsEnv!.registerDecorator('decorator', () => { + calls++; + }); - kbnHandlebarsEnv!.registerDecorator('decorator', () => { - calls++; - }); + const compile = kbnHandlebarsEnv![compileName].bind(kbnHandlebarsEnv); + const render = compile('{{*decorator}}'); - let renderAST; - let renderEval; - if (process.env.AST || !process.env.EVAL) { - renderAST = kbnHandlebarsEnv!.compileAST('{{*decorator}}'); - } - if (process.env.EVAL || !process.env.AST) { - renderEval = kbnHandlebarsEnv!.compile('{{*decorator}}'); - } + let calls = 0; + expect(render({})).toEqual(''); + expect(calls).toEqual(1); - let calls = 0; - if (renderAST) expect(renderAST({})).toEqual(''); - if (renderEval) expect(renderEval({})).toEqual(''); - expect(calls).toEqual(callsExpected); + calls = 0; + expect(render({})).toEqual(''); + expect(calls).toEqual(1); - calls = 0; - if (renderAST) expect(renderAST({})).toEqual(''); - if (renderEval) expect(renderEval({})).toEqual(''); - expect(calls).toEqual(callsExpected); + global.kbnHandlebarsEnv = null; + }); }); it('should pass expected options to nested decorator', () => { @@ -207,6 +204,10 @@ describe('blocks', () => { global.kbnHandlebarsEnv = Handlebars.create(); }); + afterEach(() => { + global.kbnHandlebarsEnv = null; + }); + it('should be able to call decorators registered using the `registerDecorator` function', () => { let calls = 0; const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; @@ -228,7 +229,7 @@ describe('blocks', () => { kbnHandlebarsEnv!.unregisterDecorator('decorator'); - expectTemplate('{{*decorator}}').toThrowErrorMatchingSnapshot(); + expectTemplate('{{*decorator}}').toThrow('lookupProperty(...) is not a function'); expect(calls).toEqual(0); }); }); diff --git a/packages/kbn-handlebars/index.ts b/packages/kbn-handlebars/index.ts index 3d50331450ec8..a7ad36a9e8663 100644 --- a/packages/kbn-handlebars/index.ts +++ b/packages/kbn-handlebars/index.ts @@ -50,7 +50,7 @@ export const compileFnName: 'compile' | 'compileAST' = allowUnsafeEval() ? 'comp */ export type ExtendedCompileOptions = Pick< CompileOptions, - 'knownHelpers' | 'knownHelpersOnly' | 'strict' | 'assumeObjects' | 'noEscape' | 'data' + 'data' | 'knownHelpers' | 'knownHelpersOnly' | 'noEscape' | 'strict' | 'assumeObjects' >; /** @@ -61,7 +61,7 @@ export type ExtendedCompileOptions = Pick< */ export type ExtendedRuntimeOptions = Pick< RuntimeOptions, - 'helpers' | 'blockParams' | 'data' | 'decorators' + 'data' | 'helpers' | 'decorators' | 'blockParams' >; /** @@ -77,14 +77,14 @@ export type DecoratorFunction = ( options: any ) => any; -export interface DecoratorsHash { - [name: string]: DecoratorFunction; -} - export interface HelpersHash { [name: string]: Handlebars.HelperDelegate; } +export interface DecoratorsHash { + [name: string]: DecoratorFunction; +} + /** * Normally this namespace isn't used directly. It's required to be present by * TypeScript when calling the `Handlebars.create()` function. @@ -298,6 +298,7 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { Program(program: hbs.AST.Program) { this.blockParamNames.unshift(program.blockParams); + // Run any decorators that might exist on the root this.processDecorators(program, this.generateProgramFunction(program)); this.processedRootDecorators = true; @@ -314,13 +315,13 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { } // This space intentionally left blank: We want to override the Visitor class implementation - // of `DecoratorBlock`, but since we handle decorators separately before traversing the - // nodes, we just want to make this a no-op. + // of this method, but since we handle decorators separately before traversing the nodes, we + // just want to make this a no-op. DecoratorBlock(decorator: hbs.AST.DecoratorBlock) {} // This space intentionally left blank: We want to override the Visitor class implementation - // of `DecoratorBlock`, but since we handle decorators separately before traversing the - // nodes, we just want to make this a no-op. + // of this method, but since we handle decorators separately before traversing the nodes, we + // just want to make this a no-op. Decorator(decorator: hbs.AST.Decorator) {} SubExpression(sexpr: hbs.AST.SubExpression) { @@ -393,20 +394,23 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { ) { // TypeScript: The types indicate that `decorator.path` technically can be an `hbs.AST.Literal`. However, the upstream codebase always treats it as an `hbs.AST.PathExpression`, so we do too. const name = (decorator.path as hbs.AST.PathExpression).original; - const decoratorFn = this.container.lookupProperty( - this.container.decorators, - name - ); const props = {}; // TypeScript: Because `decorator` can be of type `hbs.AST.Decorator`, TS indicates that `decorator.path` technically can be an `hbs.AST.Literal`. However, the upstream codebase always treats it as an `hbs.AST.PathExpression`, so we do too. const options = this.setupParams(decorator as hbs.AST.DecoratorBlock, name); // @ts-expect-error: Property 'lookupProperty' does not exist on type 'HelperOptions' delete options.lookupProperty; // There's really no tests/documentation on this, but to match the upstream codebase we'll remove `lookupProperty` from the decorator context - Object.assign(decoratorFn(prog, props, this.container, options) || prog, props); + const result = this.container.lookupProperty( + this.container.decorators, + name + )(prog, props, this.container, options); + + Object.assign(result || prog, props); } private processStatementOrExpression(node: ProcessableNode) { + // Calling `transformLiteralToPath` has side-effects! + // It converts a node from type `ProcessableNode` to `ProcessableNodeWithPathParts` transformLiteralToPath(node); switch (this.classifyNode(node as ProcessableNodeWithPathParts)) { @@ -533,6 +537,7 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { private invokeKnownHelper(node: ProcessableNodeWithPathParts) { const name = node.path.parts[0]; const helper = this.setupHelper(node, name); + // TypeScript: `helper.fn` might be `undefined` at this point, but to match the upstream behavior we call it without any guards const result = helper.fn.apply(helper.context, helper.params); this.output.push(result); } @@ -558,6 +563,7 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { } } + // TypeScript: `helper.fn` might be `undefined` at this point, but to match the upstream behavior we call it without any guards const result = helper.fn.apply(helper.context, helper.params); this.output.push(result); @@ -573,8 +579,7 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { } } else { if ( - // @ts-expect-error: The `escaped` property is only on MustacheStatement nodes - node.escaped === false || + (node as hbs.AST.MustacheStatement).escaped === false || this.compileOptions.noEscape === true || typeof invokeResult !== 'string' ) { @@ -679,8 +684,9 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { nextContext: any, runtimeOptions: ExtendedRuntimeOptions = {} ) => { - // inherit data in blockParams from parent program runtimeOptions = Object.assign({}, runtimeOptions); + + // inherit data in blockParams from parent program runtimeOptions.data = runtimeOptions.data || this.runtimeOptions!.data; if (runtimeOptions.blockParams) { runtimeOptions.blockParams = runtimeOptions.blockParams.concat( diff --git a/packages/kbn-handlebars/src/__jest__/test_bench.ts b/packages/kbn-handlebars/src/__jest__/test_bench.ts index 1ee3f68c220e0..5719158661d0f 100644 --- a/packages/kbn-handlebars/src/__jest__/test_bench.ts +++ b/packages/kbn-handlebars/src/__jest__/test_bench.ts @@ -10,6 +10,11 @@ import Handlebars, { type ExtendedRuntimeOptions, } from '../..'; +type CompileFns = 'compile' | 'compileAST'; +const compileFns: CompileFns[] = ['compile', 'compileAST']; +if (process.env.AST) compileFns.splice(0, 1); +else if (process.env.EVAL) compileFns.splice(1, 1); + declare global { var kbnHandlebarsEnv: typeof Handlebars | null; // eslint-disable-line no-var } @@ -24,12 +29,18 @@ export function expectTemplate(template: string, options?: TestOptions) { return new HandlebarsTestBench(template, options); } +export function forEachCompileFunctionName( + cb: (compileName: CompileFns, index: number, array: CompileFns[]) => void +) { + compileFns.forEach(cb); +} + class HandlebarsTestBench { private template: string; private options: TestOptions; private compileOptions?: ExtendedCompileOptions; private runtimeOptions?: ExtendedRuntimeOptions; - private helpers: { [key: string]: Handlebars.HelperDelegate | undefined } = {}; + private helpers: { [name: string]: Handlebars.HelperDelegate | undefined } = {}; private decorators: DecoratorsHash = {}; private input: any = {}; @@ -58,7 +69,7 @@ class HandlebarsTestBench { return this; } - withHelpers(helperFunctions: { [key: string]: Handlebars.HelperDelegate }) { + withHelpers(helperFunctions: { [name: string]: Handlebars.HelperDelegate }) { for (const [name, helper] of Object.entries(helperFunctions)) { this.withHelper(name, helper); } @@ -108,12 +119,6 @@ class HandlebarsTestBench { } } - toThrowErrorMatchingSnapshot() { - const { renderEval, renderAST } = this.compile(); - expect(() => renderEval(this.input)).toThrowErrorMatchingSnapshot(); - expect(() => renderAST(this.input)).toThrowErrorMatchingSnapshot(); - } - private compileAndExecute() { if (process.env.EVAL) { return { @@ -159,15 +164,6 @@ class HandlebarsTestBench { return renderAST(this.input, runtimeOptions); } - private compile() { - const handlebarsEnv = getHandlebarsEnv(); - - return { - renderEval: this.compileEval(handlebarsEnv), - renderAST: this.compileAST(handlebarsEnv), - }; - } - private compileEval(handlebarsEnv = getHandlebarsEnv()) { this.execBeforeEach(); return handlebarsEnv.compile(this.template, this.compileOptions); diff --git a/packages/kbn-handlebars/src/upstream/index.blocks.test.ts b/packages/kbn-handlebars/src/upstream/index.blocks.test.ts index c2491d94a92af..52553fb04f5b8 100644 --- a/packages/kbn-handlebars/src/upstream/index.blocks.test.ts +++ b/packages/kbn-handlebars/src/upstream/index.blocks.test.ts @@ -316,6 +316,10 @@ describe('blocks', () => { global.kbnHandlebarsEnv = Handlebars.create(); }); + afterEach(() => { + global.kbnHandlebarsEnv = null; + }); + it('unregisters', () => { // @ts-expect-error: Cannot assign to 'decorators' because it is a read-only property. kbnHandlebarsEnv!.decorators = {}; diff --git a/packages/kbn-handlebars/src/upstream/index.builtins.test.ts b/packages/kbn-handlebars/src/upstream/index.builtins.test.ts index ef242260e7c75..c47ec29fff0f9 100644 --- a/packages/kbn-handlebars/src/upstream/index.builtins.test.ts +++ b/packages/kbn-handlebars/src/upstream/index.builtins.test.ts @@ -476,6 +476,8 @@ describe('builtin helpers', () => { console.log = $log; console.info = $info; console.error = $error; + + global.kbnHandlebarsEnv = null; }); it('should call logger at default level', function () { diff --git a/packages/kbn-handlebars/src/upstream/index.compiler.test.ts b/packages/kbn-handlebars/src/upstream/index.compiler.test.ts index 023e15db87ae3..3e225d30199b8 100644 --- a/packages/kbn-handlebars/src/upstream/index.compiler.test.ts +++ b/packages/kbn-handlebars/src/upstream/index.compiler.test.ts @@ -6,15 +6,11 @@ */ import Handlebars from '../..'; +import { forEachCompileFunctionName } from '../__jest__/test_bench'; describe('compiler', () => { - const compileFns = ['compile', 'compileAST']; - if (process.env.AST) compileFns.splice(0, 1); - else if (process.env.EVAL) compileFns.splice(1, 1); - - compileFns.forEach((compileName) => { - // @ts-expect-error - const compile = Handlebars[compileName]; + forEachCompileFunctionName((compileName) => { + const compile = Handlebars[compileName].bind(Handlebars); describe(`#${compileName}`, () => { it('should fail with invalid input', () => { @@ -70,7 +66,8 @@ describe('compiler', () => { }); it('should not modify the options.data property(GH-1327)', () => { - const options = { data: [{ a: 'foo' }, { a: 'bar' }] }; + // The `data` property is supposed to be a boolean, but in this test we want to ignore that + const options = { data: [{ a: 'foo' }, { a: 'bar' }] as unknown as boolean }; compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)({}); expect(JSON.stringify(options, null, 2)).toEqual( JSON.stringify({ data: [{ a: 'foo' }, { a: 'bar' }] }, null, 2) diff --git a/packages/kbn-handlebars/src/upstream/index.data.test.ts b/packages/kbn-handlebars/src/upstream/index.data.test.ts index b7604b53998da..d47f5ef41dae8 100644 --- a/packages/kbn-handlebars/src/upstream/index.data.test.ts +++ b/packages/kbn-handlebars/src/upstream/index.data.test.ts @@ -47,6 +47,8 @@ describe('data', () => { .withInput({ foo: true }) .withHelpers(helpers) .toCompileTo('Hello world'); + + global.kbnHandlebarsEnv = null; }); it('parameter data can be looked up via @foo', () => { diff --git a/packages/kbn-handlebars/src/upstream/index.helpers.test.ts b/packages/kbn-handlebars/src/upstream/index.helpers.test.ts index bb5d7b4eff653..5fd5bfad0e333 100644 --- a/packages/kbn-handlebars/src/upstream/index.helpers.test.ts +++ b/packages/kbn-handlebars/src/upstream/index.helpers.test.ts @@ -12,6 +12,10 @@ beforeEach(() => { global.kbnHandlebarsEnv = Handlebars.create(); }); +afterEach(() => { + global.kbnHandlebarsEnv = null; +}); + describe('helpers', () => { it('helper with complex lookup$', () => { expectTemplate('{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}') From aecad27159764d8ea2d0aeddc94fd03954d480e5 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 19 Dec 2022 18:35:31 +0100 Subject: [PATCH 20/55] [ML] Explain Log Rate Spikes: Fix field candidate selection. (#147614) The field candidate selection for Explain Log Rate Spikes was missing a check if the supported field type is also aggregatable. For example, a `keyword` type field could still be non-aggregatable if it was both not indexed and `doc_values` set to `false`. Additionally, if no groups were detected, we showed a "Try to continue analysis" button in the UI even if the analysis was able to finish. In this PR the artificial logs dataset for functional tests was extended to include a field like that. --- .../explain_log_rate_spikes_analysis.tsx | 5 +++-- .../server/routes/queries/fetch_index_info.test.ts | 13 ++++++++++--- .../aiops/server/routes/queries/fetch_index_info.ts | 3 ++- .../aiops/explain_log_rate_spikes_data_generator.ts | 5 +++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx index dd67151d1e2c3..4b8ad8a891961 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx @@ -112,8 +112,9 @@ export const ExplainLogRateSpikesAnalysis: FC const { loaded, remainingFieldCandidates, groupsMissing } = data; if ( - (Array.isArray(remainingFieldCandidates) && remainingFieldCandidates.length > 0) || - groupsMissing + loaded < 1 && + ((Array.isArray(remainingFieldCandidates) && remainingFieldCandidates.length > 0) || + groupsMissing) ) { setOverrides({ loaded, remainingFieldCandidates, changePoints: data.changePoints }); } else { diff --git a/x-pack/plugins/aiops/server/routes/queries/fetch_index_info.test.ts b/x-pack/plugins/aiops/server/routes/queries/fetch_index_info.test.ts index 084c415a652cd..f38feaee8a58b 100644 --- a/x-pack/plugins/aiops/server/routes/queries/fetch_index_info.test.ts +++ b/x-pack/plugins/aiops/server/routes/queries/fetch_index_info.test.ts @@ -70,9 +70,16 @@ describe('fetch_index_info', () => { it('returns field candidates and total hits', async () => { const esClientFieldCapsMock = jest.fn(() => ({ fields: { - myIpFieldName: { ip: {} }, - myKeywordFieldName: { keyword: {} }, - myUnpopulatedKeywordFieldName: { keyword: {} }, + // Should end up as a field candidate + myIpFieldName: { ip: { aggregatable: true } }, + // Should end up as a field candidate + myKeywordFieldName: { keyword: { aggregatable: true } }, + // Should not end up as a field candidate, it's a keyword but non-aggregatable + myKeywordFieldNameToBeIgnored: { keyword: { aggregatable: false } }, + // Should not end up as a field candidate, based on this field caps result it would be + // but it will not be part of the mocked search result so will count as unpopulated. + myUnpopulatedKeywordFieldName: { keyword: { aggregatable: true } }, + // Should not end up as a field candidate since fields of type number will not be considered myNumericFieldName: { number: {} }, }, })); diff --git a/x-pack/plugins/aiops/server/routes/queries/fetch_index_info.ts b/x-pack/plugins/aiops/server/routes/queries/fetch_index_info.ts index 93378911c7201..33a298af8e98f 100644 --- a/x-pack/plugins/aiops/server/routes/queries/fetch_index_info.ts +++ b/x-pack/plugins/aiops/server/routes/queries/fetch_index_info.ts @@ -68,9 +68,10 @@ export const fetchIndexInfo = async ( Object.entries(respMapping.fields).forEach(([key, value]) => { const fieldTypes = Object.keys(value) as ES_FIELD_TYPES[]; const isSupportedType = fieldTypes.some((type) => SUPPORTED_ES_FIELD_TYPES.includes(type)); + const isAggregatable = fieldTypes.some((type) => value[type].aggregatable); // Check if fieldName is something we can aggregate on - if (isSupportedType) { + if (isSupportedType && isAggregatable) { acceptableFields.add(key); } }); diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts index 2039ab7b7ae3c..6e5f7044efbc3 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts @@ -15,6 +15,7 @@ export interface GeneratedDoc { url: string; version: string; '@timestamp': number; + should_ignore_this_field: string; } const REFERENCE_TS = 1669018354793; @@ -50,6 +51,7 @@ function getArtificialLogsWithSpike(index: string) { url, version: 'v1.0.0', '@timestamp': ts + tsOffset, + should_ignore_this_field: 'should_ignore_this_field', }; bulkBody.push(action); @@ -74,6 +76,7 @@ function getArtificialLogsWithSpike(index: string) { url, version: 'v1.0.0', '@timestamp': DEVIATION_TS + tsOffset, + should_ignore_this_field: 'should_ignore_this_field', }); }); }); @@ -91,6 +94,7 @@ function getArtificialLogsWithSpike(index: string) { url, version: 'v1.0.0', '@timestamp': DEVIATION_TS + tsOffset, + should_ignore_this_field: 'should_ignore_this_field', }); }); }); @@ -158,6 +162,7 @@ export function ExplainLogRateSpikesDataGeneratorProvider({ getService }: FtrPro url: { type: 'keyword' }, version: { type: 'keyword' }, '@timestamp': { type: 'date' }, + should_ignore_this_field: { type: 'keyword', doc_values: false, index: false }, }, }, }); From 2763af3a4ecc4a75c652676967f581d145714289 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 19 Dec 2022 10:42:12 -0700 Subject: [PATCH 21/55] [ftr] remove @types/mocha, define custom ambient-ftr-types (#147284) After moving away from composite projects in the IDE we now have an issue where projects like security solutions are getting `@types/jest` and `@types/mocha` loaded up, even though the "types" compiler option in security solutions focuses on jest. To fix this I've removed the `@types/mocha` package, implemented/copied a portion of the mocha types into a new `@kbn/ambient-ftr-types` package which can be used in ftr packages to define the describe/it/etc. globals. Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .buildkite/package-lock.json | 16 +- .buildkite/package.json | 4 +- .../scripts/steps/checks/verify_notice.sh | 8 +- .github/CODEOWNERS | 1 + NOTICE.txt | 14 + package.json | 2 +- packages/BUILD.bazel | 1 + packages/kbn-ambient-ftr-types/BUILD.bazel | 58 ++ packages/kbn-ambient-ftr-types/README.md | 3 + packages/kbn-ambient-ftr-types/index.d.ts | 983 ++++++++++++++++++ packages/kbn-ambient-ftr-types/jest.config.js | 13 + packages/kbn-ambient-ftr-types/kibana.jsonc | 8 + packages/kbn-ambient-ftr-types/package.json | 8 + packages/kbn-ambient-ftr-types/tsconfig.json | 15 + packages/kbn-journeys/BUILD.bazel | 2 +- packages/kbn-journeys/tsconfig.json | 4 +- .../lib/mocha/setup_mocha.ts | 5 +- test/tsconfig.json | 1 + tsconfig.base.json | 2 + .../test/accessibility/apps/observability.ts | 2 - .../api_integration/apis/search/search.ts | 3 +- x-pack/test/fleet_api_integration/helpers.ts | 3 +- x-pack/test/tsconfig.json | 2 +- yarn.lock | 9 +- 24 files changed, 1137 insertions(+), 30 deletions(-) create mode 100644 packages/kbn-ambient-ftr-types/BUILD.bazel create mode 100644 packages/kbn-ambient-ftr-types/README.md create mode 100644 packages/kbn-ambient-ftr-types/index.d.ts create mode 100644 packages/kbn-ambient-ftr-types/jest.config.js create mode 100644 packages/kbn-ambient-ftr-types/kibana.jsonc create mode 100644 packages/kbn-ambient-ftr-types/package.json create mode 100644 packages/kbn-ambient-ftr-types/tsconfig.json diff --git a/.buildkite/package-lock.json b/.buildkite/package-lock.json index 2bf8ce04b4739..df1114379a1cb 100644 --- a/.buildkite/package-lock.json +++ b/.buildkite/package-lock.json @@ -19,10 +19,10 @@ "@types/chai": "^4.2.10", "@types/js-yaml": "^4.0.5", "@types/minimatch": "^3.0.5", - "@types/mocha": "^7.0.2", + "@types/mocha": "^8.2.3", "@types/node": "^15.12.2", "chai": "^4.2.0", - "mocha": "^8.2.1", + "mocha": "^8.4.0", "nock": "^12.0.2", "ts-node": "^10.7.0", "typescript": "^4.6.4" @@ -260,9 +260,9 @@ "dev": true }, "node_modules/@types/mocha": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-7.0.2.tgz", - "integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==", + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.3.tgz", + "integrity": "sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==", "dev": true }, "node_modules/@types/node": { @@ -1958,9 +1958,9 @@ "dev": true }, "@types/mocha": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-7.0.2.tgz", - "integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==", + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.3.tgz", + "integrity": "sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==", "dev": true }, "@types/node": { diff --git a/.buildkite/package.json b/.buildkite/package.json index c7255a565235d..797fbc9a9910d 100644 --- a/.buildkite/package.json +++ b/.buildkite/package.json @@ -18,10 +18,10 @@ "@types/chai": "^4.2.10", "@types/js-yaml": "^4.0.5", "@types/minimatch": "^3.0.5", - "@types/mocha": "^7.0.2", + "@types/mocha": "^8.2.3", "@types/node": "^15.12.2", "chai": "^4.2.0", - "mocha": "^8.2.1", + "mocha": "^8.4.0", "nock": "^12.0.2", "ts-node": "^10.7.0", "typescript": "^4.6.4" diff --git a/.buildkite/scripts/steps/checks/verify_notice.sh b/.buildkite/scripts/steps/checks/verify_notice.sh index aa21c0a6bb24a..78045cf91370c 100755 --- a/.buildkite/scripts/steps/checks/verify_notice.sh +++ b/.buildkite/scripts/steps/checks/verify_notice.sh @@ -5,4 +5,10 @@ set -euo pipefail source .buildkite/scripts/common/util.sh echo --- Verify NOTICE -node scripts/notice --validate + +if is_pr && ! is_auto_commit_disabled; then + node scripts/notice + check_for_changed_files "node scripts/notice" true +else + node scripts/notice --validate +fi diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 783ea220ef5a6..696e2501518a8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -877,6 +877,7 @@ packages/home/sample_data_types @elastic/kibana-global-experience packages/kbn-ace @elastic/platform-deployment-management packages/kbn-alerts @elastic/security-solution packages/kbn-ambient-common-types @elastic/kibana-operations +packages/kbn-ambient-ftr-types @elastic/kibana-operations packages/kbn-ambient-storybook-types @elastic/kibana-operations packages/kbn-ambient-ui-types @elastic/kibana-operations packages/kbn-analytics @elastic/kibana-core diff --git a/NOTICE.txt b/NOTICE.txt index adbd050e0b7f6..5115746affeb2 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -21,6 +21,20 @@ used. Logarithmic ticks are places at powers of ten and at half those values if there are not to many ticks already (e.g. [1, 5, 10, 50, 100]). For details, see https://github.com/flot/flot/pull/1328 +--- +These types are extracted from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/bb83a9839cb23195f3f0ac5a0ec61af879f194e9/types/mocha +and modified for use in the Kibana repository. + +This project is licensed under the MIT license. + +Copyrights are respective of each contributor listed at the beginning of each definition file. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + --- This module was heavily inspired by the externals plugin that ships with webpack@97d58d31 MIT License http://www.opensource.org/licenses/mit-license.php diff --git a/package.json b/package.json index 4777433f07312..ae72182a5dc1c 100644 --- a/package.json +++ b/package.json @@ -733,6 +733,7 @@ "@jest/transform": "^29.3.1", "@jest/types": "^29.3.1", "@kbn/ambient-common-types": "link:bazel-bin/packages/kbn-ambient-common-types", + "@kbn/ambient-ftr-types": "link:bazel-bin/packages/kbn-ambient-ftr-types", "@kbn/ambient-storybook-types": "link:bazel-bin/packages/kbn-ambient-storybook-types", "@kbn/ambient-ui-types": "link:bazel-bin/packages/kbn-ambient-ui-types", "@kbn/apm-synthtrace": "link:bazel-bin/packages/kbn-apm-synthtrace", @@ -898,7 +899,6 @@ "@types/mime-types": "^2.1.0", "@types/minimatch": "^2.0.29", "@types/minimist": "^1.2.2", - "@types/mocha": "^9.1.1", "@types/mock-fs": "^4.13.1", "@types/moment-duration-format": "^2.2.3", "@types/mustache": "^0.8.31", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 2570dd42525ea..010a992a61bf1 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -196,6 +196,7 @@ filegroup( "//packages/kbn-ace:build", "//packages/kbn-alerts:build", "//packages/kbn-ambient-common-types:build", + "//packages/kbn-ambient-ftr-types:build", "//packages/kbn-ambient-storybook-types:build", "//packages/kbn-ambient-ui-types:build", "//packages/kbn-analytics:build", diff --git a/packages/kbn-ambient-ftr-types/BUILD.bazel b/packages/kbn-ambient-ftr-types/BUILD.bazel new file mode 100644 index 0000000000000..f6de3cba29f6a --- /dev/null +++ b/packages/kbn-ambient-ftr-types/BUILD.bazel @@ -0,0 +1,58 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "pkg_npm") + +PKG_DIRNAME = "kbn-ambient-ftr-types" +PKG_REQUIRE_NAME = "@kbn/ambient-ftr-types" + +SRCS = glob( + [ + "*.d.ts", + ] +) + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ +] + +js_library( + name = PKG_DIRNAME, + srcs = SRCS + NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +alias( + name = "npm_module_types", + actual = ":" + PKG_DIRNAME, + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-ambient-ftr-types/README.md b/packages/kbn-ambient-ftr-types/README.md new file mode 100644 index 0000000000000..835d0836fbe09 --- /dev/null +++ b/packages/kbn-ambient-ftr-types/README.md @@ -0,0 +1,3 @@ +# @kbn/ambient-ftr-types + +Ambient type definitions that should be included in `tsconfig.json` files for packages containing FTR test suites. diff --git a/packages/kbn-ambient-ftr-types/index.d.ts b/packages/kbn-ambient-ftr-types/index.d.ts new file mode 100644 index 0000000000000..ffc0f85058d29 --- /dev/null +++ b/packages/kbn-ambient-ftr-types/index.d.ts @@ -0,0 +1,983 @@ +/* eslint-disable @kbn/eslint/require-license-header */ +/* eslint-disable @typescript-eslint/unified-signatures */ + +/** + * @notice + * + * These types are extracted from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/bb83a9839cb23195f3f0ac5a0ec61af879f194e9/types/mocha + * and modified for use in the Kibana repository. + * + * This project is licensed under the MIT license. + * + * Copyrights are respective of each contributor listed at the beginning of each definition file. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +declare namespace Mocha { + /** + * Test context + * + * @see https://mochajs.org/api/module-Context.html#~Context + */ + interface Context { + /** + * Get test timeout. + */ + timeout(): number; + + /** + * Set test timeout. + */ + timeout(ms: string | number): this; + + /** + * Get test slowness threshold. + */ + slow(): number; + + /** + * Set test slowness threshold. + */ + slow(ms: string | number): this; + + /** + * Mark a test as skipped. + */ + skip(): never; + + /** + * Get the number of allowed retries on failed tests. + */ + retries(): number; + + /** + * Set the number of allowed retries on failed tests. + */ + retries(n: number): this; + + [key: string]: any; + } + + type Done = (err?: any) => void; + + /** + * Callback function used for tests and hooks. + */ + type Func = (this: Context, done: Done) => void; + + /** + * Async callback function used for tests and hooks. + */ + type AsyncFunc = (this: Context) => PromiseLike; + + interface Runnable { + id: string; + title: string; + fn: Func | AsyncFunc | undefined; + body: string; + async: boolean; + sync: boolean; + timedOut: boolean; + pending: boolean; + duration?: number | undefined; + parent?: Suite | undefined; + state?: 'failed' | 'passed' | 'pending' | undefined; + timer?: any; + ctx?: Context | undefined; + callback?: Done | undefined; + allowUncaught?: boolean | undefined; + file?: string | undefined; + + /** + * Get test timeout. + * + * @see https://mochajs.org/api/Runnable.html#timeout + */ + timeout(): number; + + /** + * Set test timeout. + * + * @see https://mochajs.org/api/Runnable.html#timeout + */ + timeout(ms: string | number): this; + + /** + * Get test slowness threshold. + * + * @see https://mochajs.org/api/Runnable.html#slow + */ + slow(): number; + + /** + * Set test slowness threshold. + * + * @see https://mochajs.org/api/Runnable.html#slow + */ + slow(ms: string | number): this; + + /** + * Halt and mark as pending. + */ + skip(): never; + + /** + * Check if this runnable or its parent suite is marked as pending. + * + * @see https://mochajs.org/api/Runnable.html#isPending + */ + isPending(): boolean; + + /** + * Return `true` if this Runnable has failed. + */ + isFailed(): boolean; + + /** + * Return `true` if this Runnable has passed. + */ + isPassed(): boolean; + + /** + * Set or get number of retries. + * + * @see https://mochajs.org/api/Runnable.html#retries + */ + retries(): number; + + /** + * Set or get number of retries. + * + * @see https://mochajs.org/api/Runnable.html#retries + */ + retries(n: number): void; + + /** + * Set or get current retry + * + * @see https://mochajs.org/api/Runnable.html#currentRetry + */ + protected currentRetry(): number; + + /** + * Set or get current retry + * + * @see https://mochajs.org/api/Runnable.html#currentRetry + */ + protected currentRetry(n: number): void; + + /** + * Return the full title generated by recursively concatenating the parent's full title. + */ + fullTitle(): string; + + /** + * Return the title path generated by concatenating the parent's title path with the title. + */ + titlePath(): string[]; + + /** + * Clear the timeout. + * + * @see https://mochajs.org/api/Runnable.html#clearTimeout + */ + clearTimeout(): void; + + /** + * Inspect the runnable void of private properties. + * + * @see https://mochajs.org/api/Runnable.html#inspect + */ + inspect(): string; + + /** + * Reset the timeout. + * + * @see https://mochajs.org/api/Runnable.html#resetTimeout + */ + resetTimeout(): void; + + /** + * Get a list of whitelisted globals for this test run. + * + * @see https://mochajs.org/api/Runnable.html#globals + */ + globals(): string[]; + + /** + * Set a list of whitelisted globals for this test run. + * + * @see https://mochajs.org/api/Runnable.html#globals + */ + globals(globals: readonly string[]): void; + + /** + * Run the test and invoke `fn(err)`. + * + * @see https://mochajs.org/api/Runnable.html#run + */ + run(fn: Done): void; + } + + interface Suite { + ctx: Context; + suites: Suite[]; + tests: Test[]; + pending: boolean; + file?: string | undefined; + root: boolean; + delayed: boolean; + parent: Suite | undefined; + title: string; + + /** + * Return a clone of this `Suite`. + * + * @see https://mochajs.org/api/Mocha.Suite.html#clone + */ + clone(): Suite; + + /** + * Get timeout `ms`. + * + * @see https://mochajs.org/api/Mocha.Suite.html#timeout + */ + timeout(): number; + + /** + * Set timeout `ms` or short-hand such as "2s". + * + * @see https://mochajs.org/api/Mocha.Suite.html#timeout + */ + timeout(ms: string | number): this; + + /** + * Get number of times to retry a failed test. + * + * @see https://mochajs.org/api/Mocha.Suite.html#retries + */ + retries(): number; + + /** + * Set number of times to retry a failed test. + * + * @see https://mochajs.org/api/Mocha.Suite.html#retries + */ + retries(n: string | number): this; + + /** + * Get slow `ms`. + * + * @see https://mochajs.org/api/Mocha.Suite.html#slow + */ + slow(): number; + + /** + * Set slow `ms` or short-hand such as "2s". + * + * @see https://mochajs.org/api/Mocha.Suite.html#slow + */ + slow(ms: string | number): this; + + /** + * Get whether to bail after first error. + * + * @see https://mochajs.org/api/Mocha.Suite.html#bail + */ + bail(): boolean; + + /** + * Set whether to bail after first error. + * + * @see https://mochajs.org/api/Mocha.Suite.html#bail + */ + bail(bail: boolean): this; + + /** + * Check if this suite or its parent suite is marked as pending. + * + * @see https://mochajs.org/api/Mocha.Suite.html#isPending + */ + isPending(): boolean; + + /** + * Run `fn(test[, done])` before running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeAll + */ + beforeAll(fn?: Func): this; + + /** + * Run `fn(test[, done])` before running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeAll + */ + beforeAll(fn?: AsyncFunc): this; + + /** + * Run `fn(test[, done])` before running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeAll + */ + beforeAll(title: string, fn?: Func): this; + + /** + * Run `fn(test[, done])` before running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeAll + */ + beforeAll(title: string, fn?: AsyncFunc): this; + + /** + * Run `fn(test[, done])` after running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterAll + */ + afterAll(fn?: Func): this; + + /** + * Run `fn(test[, done])` after running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterAll + */ + afterAll(fn?: AsyncFunc): this; + + /** + * Run `fn(test[, done])` after running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterAll + */ + afterAll(title: string, fn?: Func): this; + + /** + * Run `fn(test[, done])` after running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterAll + */ + afterAll(title: string, fn?: AsyncFunc): this; + + /** + * Run `fn(test[, done])` before each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeEach + */ + beforeEach(fn?: Func): this; + + /** + * Run `fn(test[, done])` before each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeEach + */ + beforeEach(fn?: AsyncFunc): this; + + /** + * Run `fn(test[, done])` before each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeEach + */ + beforeEach(title: string, fn?: Func): this; + + /** + * Run `fn(test[, done])` before each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeEach + */ + beforeEach(title: string, fn?: AsyncFunc): this; + + /** + * Run `fn(test[, done])` after each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterEach + */ + afterEach(fn?: Func): this; + + /** + * Run `fn(test[, done])` after each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterEach + */ + afterEach(fn?: AsyncFunc): this; + + /** + * Run `fn(test[, done])` after each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterEach + */ + afterEach(title: string, fn?: Func): this; + + /** + * Run `fn(test[, done])` after each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterEach + */ + afterEach(title: string, fn?: AsyncFunc): this; + + /** + * Add a test `suite`. + * + * @see https://mochajs.org/api/Mocha.Suite.html#addSuite + */ + addSuite(suite: Suite): this; + + /** + * Add a `test` to this suite. + * + * @see https://mochajs.org/api/Mocha.Suite.html#addTest + */ + addTest(test: Test): this; + + /** + * Cleans all references from this suite and all child suites. + * + * https://mochajs.org/api/suite#dispose + */ + dispose(): void; + + /** + * Return the full title generated by recursively concatenating the parent's + * full title. + * + * @see https://mochajs.org/api/Mocha.Suite.html#.Suite#fullTitle + */ + fullTitle(): string; + + /** + * Return the title path generated by recursively concatenating the parent's + * title path. + * + * @see https://mochajs.org/api/Mocha.Suite.html#.Suite#titlePath + */ + titlePath(): string[]; + + /** + * Return the total number of tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#.Suite#total + */ + total(): number; + + /** + * Iterates through each suite recursively to find all tests. Applies a + * function in the format `fn(test)`. + * + * @see https://mochajs.org/api/Mocha.Suite.html#eachTest + */ + eachTest(fn: (test: Test) => void): this; + + /** + * This will run the root suite if we happen to be running in delayed mode. + * + * @see https://mochajs.org/api/Mocha.Suite.html#run + */ + run(): void; + + /** + * Attach the given tag(s) to this suite so that the suite can be applied + * via `--include-tag` or `--exclude-tag` CLI flags. + */ + tags(tags: string | string[]): void; + + /** + * A required version range for ES. When these tests are run against an ES + * instance that is not within the given version range, the tests in this + * suite will be automatically skipped. + * + * @param {string} semver A semver version range, like ">=8". + * See https://docs.npmjs.com/cli/v6/using-npm/semver#ranges for + * information about syntax + */ + onlyEsVersion(semver: string): void; + } + + /** + * Initialize a new `Test` with the given `title` and callback `fn`. + * + * @see https://mochajs.org/api/Test.html + */ + interface Test extends Runnable { + type: 'test'; + speed?: 'slow' | 'medium' | 'fast' | undefined; // added by reporters + err?: Error | undefined; // added by reporters + clone(): Test; + } + + interface HookFunction { + /** + * [bdd, qunit, tdd] Describe a "hook" to execute the given callback `fn`. The name of the + * function is used as the name of the hook. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: Func): void; + + /** + * [bdd, qunit, tdd] Describe a "hook" to execute the given callback `fn`. The name of the + * function is used as the name of the hook. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: AsyncFunc): void; + + /** + * [bdd, qunit, tdd] Describe a "hook" to execute the given `title` and callback `fn`. + * + * - _Only available when invoked via the mocha CLI._ + */ + (name: string, fn?: Func): void; + + /** + * [bdd, qunit, tdd] Describe a "hook" to execute the given `title` and callback `fn`. + * + * - _Only available when invoked via the mocha CLI._ + */ + (name: string, fn?: AsyncFunc): void; + } + + interface SuiteFunction { + /** + * [bdd, tdd] Describe a "suite" with the given `title` and callback `fn` containing + * nested suites. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn: (this: Suite) => void): Suite; + + /** + * [qunit] Describe a "suite" with the given `title`. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string): Suite; + + /** + * [bdd, tdd, qunit] Indicates this suite should be executed exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + only: ExclusiveSuiteFunction; + + /** + * [bdd, tdd] Indicates this suite should not be executed. + * + * - _Only available when invoked via the mocha CLI._ + */ + skip: PendingSuiteFunction; + } + + interface ExclusiveSuiteFunction { + /** + * [bdd, tdd] Describe a "suite" with the given `title` and callback `fn` containing + * nested suites. Indicates this suite should be executed exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn: (this: Suite) => void): Suite; + + /** + * [qunit] Describe a "suite" with the given `title`. Indicates this suite should be executed + * exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string): Suite; + } + + /** + * [bdd, tdd] Describe a "suite" with the given `title` and callback `fn` containing + * nested suites. Indicates this suite should not be executed. + * + * - _Only available when invoked via the mocha CLI._ + * + * @returns [bdd] `Suite` + * @returns [tdd] `void` + */ + type PendingSuiteFunction = (title: string, fn: (this: Suite) => void) => Suite | void; + + interface TestFunction { + /** + * Describe a specification or test-case with the given callback `fn` acting as a thunk. + * The name of the function is used as the name of the test. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: Func): Test; + + /** + * Describe a specification or test-case with the given callback `fn` acting as a thunk. + * The name of the function is used as the name of the test. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: AsyncFunc): Test; + + /** + * Describe a specification or test-case with the given `title` and callback `fn` acting + * as a thunk. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn?: Func): Test; + + /** + * Describe a specification or test-case with the given `title` and callback `fn` acting + * as a thunk. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn?: AsyncFunc): Test; + + /** + * Indicates this test should be executed exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + only: ExclusiveTestFunction; + + /** + * Indicates this test should not be executed. + * + * - _Only available when invoked via the mocha CLI._ + */ + skip: PendingTestFunction; + + /** + * Number of attempts to retry. + * + * - _Only available when invoked via the mocha CLI._ + */ + retries(n: number): void; + } + + interface ExclusiveTestFunction { + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given callback `fn` + * acting as a thunk. The name of the function is used as the name of the test. Indicates + * this test should be executed exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: Func): Test; + + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given callback `fn` + * acting as a thunk. The name of the function is used as the name of the test. Indicates + * this test should be executed exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: AsyncFunc): Test; + + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given `title` and + * callback `fn` acting as a thunk. Indicates this test should be executed exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn?: Func): Test; + + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given `title` and + * callback `fn` acting as a thunk. Indicates this test should be executed exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn?: AsyncFunc): Test; + } + + interface PendingTestFunction { + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given callback `fn` + * acting as a thunk. The name of the function is used as the name of the test. Indicates + * this test should not be executed. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: Func): Test; + + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given callback `fn` + * acting as a thunk. The name of the function is used as the name of the test. Indicates + * this test should not be executed. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: AsyncFunc): Test; + + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given `title` and + * callback `fn` acting as a thunk. Indicates this test should not be executed. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn?: Func): Test; + + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given `title` and + * callback `fn` acting as a thunk. Indicates this test should not be executed. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn?: AsyncFunc): Test; + } + + /** + * Execute after each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#afterEach + */ + let afterEach: HookFunction; + + /** + * Execute after running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#after + */ + let after: HookFunction; + + /** + * Execute before each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#beforeEach + */ + let beforeEach: HookFunction; + + /** + * Execute before running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#before + */ + let before: HookFunction; + + /** + * Describe a "suite" containing nested suites and tests. + * + * - _Only available when invoked via the mocha CLI._ + */ + let describe: SuiteFunction; + + /** + * Describe a pending suite. + * + * - _Only available when invoked via the mocha CLI._ + */ + let xdescribe: PendingSuiteFunction; + + /** + * Describes a test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + let it: TestFunction; + + /** + * Describes a pending test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + let xit: PendingTestFunction; + + /** + * Execute before each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#beforeEach + */ + let setup: HookFunction; + + /** + * Execute before running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#before + */ + let suiteSetup: HookFunction; + + /** + * Execute after running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#after + */ + let suiteTeardown: HookFunction; + + /** + * Describe a "suite" containing nested suites and tests. + * + * - _Only available when invoked via the mocha CLI._ + */ + let suite: SuiteFunction; + + /** + * Execute after each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#afterEach + */ + let teardown: HookFunction; + + /** + * Describes a test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + let test: TestFunction; +} + +/** + * Execute before running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#before + */ +declare let before: Mocha.HookFunction; + +/** + * Execute before running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#before + */ +declare let suiteSetup: Mocha.HookFunction; + +/** + * Execute after running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#after + */ +declare let after: Mocha.HookFunction; + +/** + * Execute after running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#after + */ +declare let suiteTeardown: Mocha.HookFunction; + +/** + * Execute before each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#beforeEach + */ +declare let beforeEach: Mocha.HookFunction; + +/** + * Execute before each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#beforeEach + */ +declare let setup: Mocha.HookFunction; + +/** + * Execute after each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#afterEach + */ +declare let afterEach: Mocha.HookFunction; + +/** + * Execute after each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#afterEach + */ +declare let teardown: Mocha.HookFunction; + +/** + * Describe a "suite" containing nested suites and tests. + * + * - _Only available when invoked via the mocha CLI._ + */ +declare let describe: Mocha.SuiteFunction; + +/** + * Describe a "suite" containing nested suites and tests. + * + * - _Only available when invoked via the mocha CLI._ + */ +declare let context: Mocha.SuiteFunction; + +/** + * Describe a "suite" containing nested suites and tests. + * + * - _Only available when invoked via the mocha CLI._ + */ +declare let suite: Mocha.SuiteFunction; + +/** + * Pending suite. + * + * - _Only available when invoked via the mocha CLI._ + */ +declare let xdescribe: Mocha.PendingSuiteFunction; + +/** + * Pending suite. + * + * - _Only available when invoked via the mocha CLI._ + */ +declare let xcontext: Mocha.PendingSuiteFunction; + +/** + * Describes a test case. + * + * - _Only available when invoked via the mocha CLI._ + */ +declare let it: Mocha.TestFunction; + +/** + * Describes a test case. + * + * - _Only available when invoked via the mocha CLI._ + */ +declare let specify: Mocha.TestFunction; + +/** + * Describes a test case. + * + * - _Only available when invoked via the mocha CLI._ + */ +declare let test: Mocha.TestFunction; + +/** + * Describes a pending test case. + * + * - _Only available when invoked via the mocha CLI._ + */ +declare let xit: Mocha.PendingTestFunction; + +/** + * Describes a pending test case. + * + * - _Only available when invoked via the mocha CLI._ + */ +declare let xspecify: Mocha.PendingTestFunction; diff --git a/packages/kbn-ambient-ftr-types/jest.config.js b/packages/kbn-ambient-ftr-types/jest.config.js new file mode 100644 index 0000000000000..448fc3f2bd9e3 --- /dev/null +++ b/packages/kbn-ambient-ftr-types/jest.config.js @@ -0,0 +1,13 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-ambient-ftr-types'], +}; diff --git a/packages/kbn-ambient-ftr-types/kibana.jsonc b/packages/kbn-ambient-ftr-types/kibana.jsonc new file mode 100644 index 0000000000000..5fa351e4d7814 --- /dev/null +++ b/packages/kbn-ambient-ftr-types/kibana.jsonc @@ -0,0 +1,8 @@ +{ + "type": "shared-common", + "id": "@kbn/ambient-ftr-types", + "owner": "@elastic/kibana-operations", + "devOnly": true, + "runtimeDeps": [], + "typeDeps": [], +} diff --git a/packages/kbn-ambient-ftr-types/package.json b/packages/kbn-ambient-ftr-types/package.json new file mode 100644 index 0000000000000..7928b304529cf --- /dev/null +++ b/packages/kbn-ambient-ftr-types/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/ambient-ftr-types", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/kbn-ambient-ftr-types/tsconfig.json b/packages/kbn-ambient-ftr-types/tsconfig.json new file mode 100644 index 0000000000000..292157c18591a --- /dev/null +++ b/packages/kbn-ambient-ftr-types/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ] +} diff --git a/packages/kbn-journeys/BUILD.bazel b/packages/kbn-journeys/BUILD.bazel index b6c6f0ed2fbf2..298b36dbca2e3 100644 --- a/packages/kbn-journeys/BUILD.bazel +++ b/packages/kbn-journeys/BUILD.bazel @@ -59,7 +59,6 @@ RUNTIME_DEPS = [ # References to NPM packages work the same as RUNTIME_DEPS TYPES_DEPS = [ "@npm//@types/node", - "@npm//@types/mocha", "@npm//playwright", "@npm//uuid", "@npm//axios", @@ -70,6 +69,7 @@ TYPES_DEPS = [ "//packages/kbn-ftr-screenshot-filename:npm_module_types", "//packages/kbn-test:npm_module_types", "//packages/kbn-utils:npm_module_types", + "//packages/kbn-ambient-ftr-types:npm_module_types", ] jsts_transpiler( diff --git a/packages/kbn-journeys/tsconfig.json b/packages/kbn-journeys/tsconfig.json index d625ae13bf409..7e77ca978f3e8 100644 --- a/packages/kbn-journeys/tsconfig.json +++ b/packages/kbn-journeys/tsconfig.json @@ -5,8 +5,8 @@ "emitDeclarationOnly": true, "outDir": "target_types", "types": [ - "mocha", - "node" + "node", + "@kbn/ambient-ftr-types", ] }, "include": [ diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.ts b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.ts index ae42945b6bfdb..e2a29f8d15717 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.ts @@ -10,6 +10,8 @@ import { relative } from 'path'; import { REPO_ROOT } from '@kbn/utils'; import { ToolingLog } from '@kbn/tooling-log'; +// @ts-expect-error we don't use @types/mocha so it doesn't conflict with @types/jest +import Mocha from 'mocha'; import { Suite } from '../../fake_mocha_types'; import { loadTests } from './load_tests'; @@ -32,9 +34,6 @@ interface Options { reporterOptions?: any; } -// we use require so that @types/mocha isn't loaded -const Mocha = require('mocha'); // eslint-disable-line @typescript-eslint/no-var-requires - /** * Instantiate mocha and load testfiles into it * @return {Promise} diff --git a/test/tsconfig.json b/test/tsconfig.json index 1b5cf6f7a0eb2..904735349c3ad 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -9,6 +9,7 @@ "node", "@emotion/react/types/css-prop", "@kbn/ambient-ui-types", + "@kbn/ambient-ftr-types", ] }, "include": [ diff --git a/tsconfig.base.json b/tsconfig.base.json index c4770118c8385..d4f127712d496 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -380,6 +380,8 @@ "@kbn/alerts/*": ["packages/kbn-alerts/*"], "@kbn/ambient-common-types": ["packages/kbn-ambient-common-types"], "@kbn/ambient-common-types/*": ["packages/kbn-ambient-common-types/*"], + "@kbn/ambient-ftr-types": ["packages/kbn-ambient-ftr-types"], + "@kbn/ambient-ftr-types/*": ["packages/kbn-ambient-ftr-types/*"], "@kbn/ambient-storybook-types": ["packages/kbn-ambient-storybook-types"], "@kbn/ambient-storybook-types/*": ["packages/kbn-ambient-storybook-types/*"], "@kbn/ambient-ui-types": ["packages/kbn-ambient-ui-types"], diff --git a/x-pack/test/accessibility/apps/observability.ts b/x-pack/test/accessibility/apps/observability.ts index ee3e3a61a0ba6..ead89c913d1e1 100644 --- a/x-pack/test/accessibility/apps/observability.ts +++ b/x-pack/test/accessibility/apps/observability.ts @@ -6,8 +6,6 @@ */ // a11y tests for spaces, space selection and space creation and feature controls - -import { describe } from 'mocha'; import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/search/search.ts b/x-pack/test/api_integration/apis/search/search.ts index e7dfbb52ec701..f4f3c2e414003 100644 --- a/x-pack/test/api_integration/apis/search/search.ts +++ b/x-pack/test/api_integration/apis/search/search.ts @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import type { Context } from 'mocha'; import { parse as parseCookie } from 'tough-cookie'; import { FtrProviderContext } from '../../ftr_provider_context'; import { verifyErrorResponse } from '../../../../../test/api_integration/apis/search/verify_error'; @@ -30,7 +29,7 @@ export default function ({ getService }: FtrProviderContext) { }, }); - async function markRequiresShardDelayAgg(testContext: Context) { + async function markRequiresShardDelayAgg(testContext: Mocha.Context) { const body = await es.info(); if (!body.version.number.includes('SNAPSHOT')) { log.debug('Skipping because this build does not have the required shard_delay agg'); diff --git a/x-pack/test/fleet_api_integration/helpers.ts b/x-pack/test/fleet_api_integration/helpers.ts index c21a9e01b3309..c0f1543ee6e85 100644 --- a/x-pack/test/fleet_api_integration/helpers.ts +++ b/x-pack/test/fleet_api_integration/helpers.ts @@ -5,11 +5,10 @@ * 2.0. */ -import { Context } from 'mocha'; import { ToolingLog } from '@kbn/tooling-log'; import { FtrProviderContext } from '../api_integration/ftr_provider_context'; -export function warnAndSkipTest(mochaContext: Context, log: ToolingLog) { +export function warnAndSkipTest(mochaContext: Mocha.Context, log: ToolingLog) { log.warning( 'disabling tests because DockerServers service is not enabled, set FLEET_PACKAGE_REGISTRY_PORT to run them' ); diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 402b915247fc3..f825153c69d20 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -5,7 +5,7 @@ "emitDeclarationOnly": true, "declaration": true, "declarationMap": true, - "types": ["node"], + "types": ["node", "@kbn/ambient-ftr-types"], // there is still a decent amount of JS in this plugin and we are taking // advantage of the fact that TS doesn't know the types of that code and // gives us `any`. Once that code is converted to .ts we can remove this diff --git a/yarn.lock b/yarn.lock index 1f0aa7001063c..69a3a6ae1ab01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2709,6 +2709,10 @@ version "0.0.0" uid "" +"@kbn/ambient-ftr-types@link:bazel-bin/packages/kbn-ambient-ftr-types": + version "0.0.0" + uid "" + "@kbn/ambient-storybook-types@link:bazel-bin/packages/kbn-ambient-storybook-types": version "0.0.0" uid "" @@ -7194,11 +7198,6 @@ dependencies: "@types/node" "*" -"@types/mocha@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" - integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== - "@types/mock-fs@^4.13.1": version "4.13.1" resolved "https://registry.yarnpkg.com/@types/mock-fs/-/mock-fs-4.13.1.tgz#9201554ceb23671badbfa8ac3f1fa9e0706305be" From a67f30b46df637b5a0c8abde4b6e29f948b7838f Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 19 Dec 2022 12:51:52 -0500 Subject: [PATCH 22/55] [Synthetics] organize uptime and synthetics e2e tests (#147782) ## Summary Organize Synthetics e2e tests into Uptime and Synthetics folders Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Shahzad --- .../plugins/synthetics/e2e/journeys/index.ts | 14 +------------- .../synthetics/add_monitor.journey.ts | 2 +- .../synthetics/alerting_default.journey.ts | 2 +- .../{ => synthetics}/detail_flyout.ts | 2 +- .../synthetics/getting_started.journey.ts | 2 +- .../synthetics/global_parameters.journey.ts | 2 +- .../e2e/journeys/synthetics/index.ts | 1 + .../synthetics/management_list.journey.ts | 2 +- .../synthetics/monitor_selector.journey.ts | 2 +- .../synthetics/overview_scrolling.journey.ts | 2 +- .../synthetics/overview_search.journey.ts | 2 +- .../synthetics/overview_sorting.journey.ts | 2 +- .../synthetics/private_locations.journey.ts | 2 +- .../alerts/default_email_settings.ts | 2 +- .../e2e/journeys/{ => uptime}/alerts/index.ts | 0 .../status_alert_flyouts_in_alerting_app.ts | 2 +- .../tls_alert_flyouts_in_alerting_app.ts | 2 +- .../{ => uptime}/data_view_permissions.ts | 2 +- .../synthetics/e2e/journeys/uptime/index.ts | 19 +++++++++++++++++++ .../journeys/{ => uptime}/locations/index.ts | 0 .../{ => uptime}/locations/locations.ts | 4 ++-- .../{ => uptime}/monitor_details.journey.ts | 2 +- .../{ => uptime}/monitor_details/index.ts | 0 .../monitor_details/monitor_alerts.journey.ts | 2 +- .../monitor_details.journey.ts | 2 +- .../monitor_details/ping_redirects.journey.ts | 4 ++-- .../monitor_management.journey.ts | 4 ++-- .../monitor_management_enablement.journey.ts | 2 +- .../{ => uptime}/monitor_name.journey.ts | 2 +- .../add_monitor_private_location.ts | 2 +- .../{ => uptime}/private_locations/index.ts | 0 .../private_locations/manage_locations.ts | 2 +- .../{ => uptime}/read_only_user/index.ts | 0 .../read_only_user/monitor_management.ts | 2 +- .../{ => uptime}/step_duration.journey.ts | 2 +- .../journeys/{ => uptime}/uptime.journey.ts | 0 .../{ => synthetics}/synthetics_app.tsx | 6 +++--- .../{ => uptime}/monitor_details.tsx | 0 .../{ => uptime}/monitor_management.tsx | 6 +++--- .../page_objects/{ => uptime}/settings.tsx | 4 ++-- 40 files changed, 59 insertions(+), 51 deletions(-) rename x-pack/plugins/synthetics/e2e/journeys/{ => synthetics}/detail_flyout.ts (95%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/alerts/default_email_settings.ts (97%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/alerts/index.ts (100%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/alerts/status_alert_flyouts_in_alerting_app.ts (97%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/alerts/tls_alert_flyouts_in_alerting_app.ts (95%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/data_view_permissions.ts (96%) create mode 100644 x-pack/plugins/synthetics/e2e/journeys/uptime/index.ts rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/locations/index.ts (100%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/locations/locations.ts (94%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/monitor_details.journey.ts (95%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/monitor_details/index.ts (100%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/monitor_details/monitor_alerts.journey.ts (97%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/monitor_details/monitor_details.journey.ts (95%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/monitor_details/ping_redirects.journey.ts (93%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/monitor_management.journey.ts (98%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/monitor_management_enablement.journey.ts (94%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/monitor_name.journey.ts (95%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/private_locations/add_monitor_private_location.ts (96%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/private_locations/index.ts (100%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/private_locations/manage_locations.ts (96%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/read_only_user/index.ts (100%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/read_only_user/monitor_management.ts (90%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/step_duration.journey.ts (96%) rename x-pack/plugins/synthetics/e2e/journeys/{ => uptime}/uptime.journey.ts (100%) rename x-pack/plugins/synthetics/e2e/page_objects/{ => synthetics}/synthetics_app.tsx (98%) rename x-pack/plugins/synthetics/e2e/page_objects/{ => uptime}/monitor_details.tsx (100%) rename x-pack/plugins/synthetics/e2e/page_objects/{ => uptime}/monitor_management.tsx (98%) rename x-pack/plugins/synthetics/e2e/page_objects/{ => uptime}/settings.tsx (93%) diff --git a/x-pack/plugins/synthetics/e2e/journeys/index.ts b/x-pack/plugins/synthetics/e2e/journeys/index.ts index a5c002cfd6880..28ea52cca8e65 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/index.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/index.ts @@ -5,17 +5,5 @@ * 2.0. */ -export * from './data_view_permissions'; -export * from './read_only_user'; export * from './synthetics'; -export * from './alerts'; -export * from './uptime.journey'; -export * from './step_duration.journey'; -export * from './monitor_details.journey'; -export * from './monitor_name.journey'; -export * from './monitor_management.journey'; -export * from './monitor_management_enablement.journey'; -export * from './monitor_details'; -export * from './locations'; -export * from './private_locations'; -export * from './detail_flyout'; +export * from './uptime'; diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/add_monitor.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/add_monitor.journey.ts index d37d682295e4c..ae78aaa3925f7 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/add_monitor.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/add_monitor.journey.ts @@ -7,7 +7,7 @@ import uuid from 'uuid'; import { journey, step, expect, Page } from '@elastic/synthetics'; import { FormMonitorType } from '../../../common/runtime_types'; -import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app'; +import { syntheticsAppPageProvider } from '../../page_objects/synthetics/synthetics_app'; const customLocation = process.env.SYNTHETICS_TEST_LOCATION; diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/alerting_default.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/alerting_default.journey.ts index 0895ff0b6d9ff..755c3a340574f 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/alerting_default.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/alerting_default.journey.ts @@ -7,7 +7,7 @@ import { journey, step, expect, before, after } from '@elastic/synthetics'; import { byTestId } from '@kbn/observability-plugin/e2e/utils'; -import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app'; +import { syntheticsAppPageProvider } from '../../page_objects/synthetics/synthetics_app'; import { cleanSettings } from './services/settings'; journey('AlertingDefaults', async ({ page, params }) => { diff --git a/x-pack/plugins/synthetics/e2e/journeys/detail_flyout.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/detail_flyout.ts similarity index 95% rename from x-pack/plugins/synthetics/e2e/journeys/detail_flyout.ts rename to x-pack/plugins/synthetics/e2e/journeys/synthetics/detail_flyout.ts index 2f9daf13e1d03..9018e9e8b5869 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/detail_flyout.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/detail_flyout.ts @@ -6,7 +6,7 @@ */ import { before, expect, journey, step } from '@elastic/synthetics'; -import { syntheticsAppPageProvider } from '../page_objects/synthetics_app'; +import { syntheticsAppPageProvider } from '../../page_objects/synthetics/synthetics_app'; journey('Test Monitor Detail Flyout', async ({ page, params }) => { const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts index bffc68911b2f6..59cd3f00b962a 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts @@ -6,7 +6,7 @@ */ import { journey, step, expect, before, Page } from '@elastic/synthetics'; -import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app'; +import { syntheticsAppPageProvider } from '../../page_objects/synthetics/synthetics_app'; import { cleanTestMonitors } from './services/add_monitor'; journey(`Getting Started Page`, async ({ page, params }: { page: Page; params: any }) => { diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/global_parameters.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/global_parameters.journey.ts index f2687a0ba8639..67db65a74e77d 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/global_parameters.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/global_parameters.journey.ts @@ -7,7 +7,7 @@ import { journey, step, before, after, expect } from '@elastic/synthetics'; import { cleanTestParams } from './services/add_monitor'; -import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app'; +import { syntheticsAppPageProvider } from '../../page_objects/synthetics/synthetics_app'; journey(`GlobalParameters`, async ({ page, params }) => { const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts index 7d2c3c960eef1..091ae31bb3293 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts @@ -16,3 +16,4 @@ export * from './overview_search.journey'; export * from './private_locations.journey'; export * from './alerting_default.journey'; export * from './global_parameters.journey'; +export * from './detail_flyout'; diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/management_list.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/management_list.journey.ts index 972fce27c56b8..1cd8dc63bc069 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/management_list.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/management_list.journey.ts @@ -11,7 +11,7 @@ import { cleanTestMonitors, enableMonitorManagedViaApi, } from './services/add_monitor'; -import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app'; +import { syntheticsAppPageProvider } from '../../page_objects/synthetics/synthetics_app'; journey(`MonitorManagementList`, async ({ page, params }) => { const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/monitor_selector.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/monitor_selector.journey.ts index ab22de07846fc..f09bfff64ba21 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/monitor_selector.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/monitor_selector.journey.ts @@ -11,7 +11,7 @@ import { cleanTestMonitors, enableMonitorManagedViaApi, } from './services/add_monitor'; -import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app'; +import { syntheticsAppPageProvider } from '../../page_objects/synthetics/synthetics_app'; journey(`MonitorSelector`, async ({ page, params }) => { const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/overview_scrolling.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/overview_scrolling.journey.ts index ca04d3d3e074d..0f2456d7349d8 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/overview_scrolling.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/overview_scrolling.journey.ts @@ -11,7 +11,7 @@ import { cleanTestMonitors, enableMonitorManagedViaApi, } from './services/add_monitor'; -import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app'; +import { syntheticsAppPageProvider } from '../../page_objects/synthetics/synthetics_app'; journey('Overview Scrolling', async ({ page, params }) => { const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/overview_search.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/overview_search.journey.ts index 1170ff26aadbd..ac436fcb6e1e0 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/overview_search.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/overview_search.journey.ts @@ -11,7 +11,7 @@ import { cleanTestMonitors, enableMonitorManagedViaApi, } from './services/add_monitor'; -import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app'; +import { syntheticsAppPageProvider } from '../../page_objects/synthetics/synthetics_app'; journey('Overview Search', async ({ page, params }) => { const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/overview_sorting.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/overview_sorting.journey.ts index 85d1aae2f8421..617c8fa3a67d9 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/overview_sorting.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/overview_sorting.journey.ts @@ -11,7 +11,7 @@ import { cleanTestMonitors, enableMonitorManagedViaApi, } from './services/add_monitor'; -import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app'; +import { syntheticsAppPageProvider } from '../../page_objects/synthetics/synthetics_app'; journey('OverviewSorting', async ({ page, params }) => { const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/private_locations.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/private_locations.journey.ts index b4480b30b4d78..5ff6ddc636154 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/private_locations.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/private_locations.journey.ts @@ -14,7 +14,7 @@ import { cleanTestMonitors, getPrivateLocations, } from './services/add_monitor'; -import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app'; +import { syntheticsAppPageProvider } from '../../page_objects/synthetics/synthetics_app'; journey(`PrivateLocationsSettings`, async ({ page, params }) => { const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/alerts/default_email_settings.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/alerts/default_email_settings.ts similarity index 97% rename from x-pack/plugins/synthetics/e2e/journeys/alerts/default_email_settings.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/alerts/default_email_settings.ts index ce19d8442d056..6afcd03a4315f 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/alerts/default_email_settings.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/alerts/default_email_settings.ts @@ -19,7 +19,7 @@ import { byTestId, waitForLoadingToFinish, } from '@kbn/observability-plugin/e2e/utils'; -import { settingsPageProvider } from '../../page_objects/settings'; +import { settingsPageProvider } from '../../../page_objects/uptime/settings'; journey('DefaultEmailSettings', async ({ page, params }) => { const settings = settingsPageProvider({ page, kibanaUrl: params.kibanaUrl }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/alerts/index.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/alerts/index.ts similarity index 100% rename from x-pack/plugins/synthetics/e2e/journeys/alerts/index.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/alerts/index.ts diff --git a/x-pack/plugins/synthetics/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/alerts/status_alert_flyouts_in_alerting_app.ts similarity index 97% rename from x-pack/plugins/synthetics/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/alerts/status_alert_flyouts_in_alerting_app.ts index 6e62d51359e9c..1c9c06d562144 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/alerts/status_alert_flyouts_in_alerting_app.ts @@ -7,7 +7,7 @@ import { journey, step, expect, before } from '@elastic/synthetics'; import { assertText, byTestId, waitForLoadingToFinish } from '@kbn/observability-plugin/e2e/utils'; -import { loginPageProvider } from '../../page_objects/login'; +import { loginPageProvider } from '../../../page_objects/login'; journey('StatusFlyoutInAlertingApp', async ({ page, params }) => { const login = loginPageProvider({ page }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/alerts/tls_alert_flyouts_in_alerting_app.ts similarity index 95% rename from x-pack/plugins/synthetics/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/alerts/tls_alert_flyouts_in_alerting_app.ts index 7a05e15eca524..593ef4d51463e 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/alerts/tls_alert_flyouts_in_alerting_app.ts @@ -7,7 +7,7 @@ import { journey, step, before } from '@elastic/synthetics'; import { assertText, byTestId, waitForLoadingToFinish } from '@kbn/observability-plugin/e2e/utils'; -import { loginPageProvider } from '../../page_objects/login'; +import { loginPageProvider } from '../../../page_objects/login'; journey('TlsFlyoutInAlertingApp', async ({ page, params }) => { const login = loginPageProvider({ page }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/data_view_permissions.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/data_view_permissions.ts similarity index 96% rename from x-pack/plugins/synthetics/e2e/journeys/data_view_permissions.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/data_view_permissions.ts index 648337218979c..5a78b2f61c45d 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/data_view_permissions.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/data_view_permissions.ts @@ -12,7 +12,7 @@ import { waitForLoadingToFinish, } from '@kbn/observability-plugin/e2e/utils'; import { callKibana } from '@kbn/apm-plugin/server/test_helpers/create_apm_users/helpers/call_kibana'; -import { loginPageProvider } from '../page_objects/login'; +import { loginPageProvider } from '../../page_objects/login'; journey('DataViewPermissions', async ({ page, params }) => { const login = loginPageProvider({ page }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/uptime/index.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/index.ts new file mode 100644 index 0000000000000..af59317232acf --- /dev/null +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/index.ts @@ -0,0 +1,19 @@ +/* + * 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 * from './data_view_permissions'; +export * from './read_only_user'; +export * from './alerts'; +export * from './uptime.journey'; +export * from './step_duration.journey'; +export * from './monitor_details.journey'; +export * from './monitor_name.journey'; +export * from './monitor_management.journey'; +export * from './monitor_management_enablement.journey'; +export * from './monitor_details'; +export * from './locations'; +export * from './private_locations'; diff --git a/x-pack/plugins/synthetics/e2e/journeys/locations/index.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/locations/index.ts similarity index 100% rename from x-pack/plugins/synthetics/e2e/journeys/locations/index.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/locations/index.ts diff --git a/x-pack/plugins/synthetics/e2e/journeys/locations/locations.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/locations/locations.ts similarity index 94% rename from x-pack/plugins/synthetics/e2e/journeys/locations/locations.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/locations/locations.ts index 5f555dd6e5070..761e08e1b9243 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/locations/locations.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/locations/locations.ts @@ -6,8 +6,8 @@ */ import { journey, step, before, Page } from '@elastic/synthetics'; -import { makeChecksWithStatus } from '../../helpers/make_checks'; -import { monitorDetailsPageProvider } from '../../page_objects/monitor_details'; +import { makeChecksWithStatus } from '../../../helpers/make_checks'; +import { monitorDetailsPageProvider } from '../../../page_objects/uptime/monitor_details'; journey('Observer location', async ({ page, params }: { page: Page; params: any }) => { const monitorDetails = monitorDetailsPageProvider({ page, kibanaUrl: params.kibanaUrl }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/monitor_details.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_details.journey.ts similarity index 95% rename from x-pack/plugins/synthetics/e2e/journeys/monitor_details.journey.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_details.journey.ts index 644c1bbdc7891..39de11dcd1e34 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/monitor_details.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_details.journey.ts @@ -13,7 +13,7 @@ */ import uuid from 'uuid'; import { journey, step, expect, after, Page } from '@elastic/synthetics'; -import { monitorManagementPageProvider } from '../page_objects/monitor_management'; +import { monitorManagementPageProvider } from '../../page_objects/uptime/monitor_management'; journey('MonitorDetails', async ({ page, params }: { page: Page; params: any }) => { const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/monitor_details/index.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_details/index.ts similarity index 100% rename from x-pack/plugins/synthetics/e2e/journeys/monitor_details/index.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_details/index.ts diff --git a/x-pack/plugins/synthetics/e2e/journeys/monitor_details/monitor_alerts.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_details/monitor_alerts.journey.ts similarity index 97% rename from x-pack/plugins/synthetics/e2e/journeys/monitor_details/monitor_alerts.journey.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_details/monitor_alerts.journey.ts index 5993e0839c839..92d3411c97f70 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/monitor_details/monitor_alerts.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_details/monitor_alerts.journey.ts @@ -8,7 +8,7 @@ import { journey, step, expect, before, Page } from '@elastic/synthetics'; import { noop } from 'lodash'; import { byTestId, delay } from '@kbn/observability-plugin/e2e/utils'; -import { monitorDetailsPageProvider } from '../../page_objects/monitor_details'; +import { monitorDetailsPageProvider } from '../../../page_objects/uptime/monitor_details'; const dateRangeStart = '2019-09-10T12:40:08.078Z'; const dateRangeEnd = '2019-09-11T19:40:08.078Z'; diff --git a/x-pack/plugins/synthetics/e2e/journeys/monitor_details/monitor_details.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_details/monitor_details.journey.ts similarity index 95% rename from x-pack/plugins/synthetics/e2e/journeys/monitor_details/monitor_details.journey.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_details/monitor_details.journey.ts index 23ea8db3ecc48..8402253cf9be8 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/monitor_details/monitor_details.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_details/monitor_details.journey.ts @@ -7,7 +7,7 @@ import { journey, step, before, Page } from '@elastic/synthetics'; import { byTestId } from '@kbn/observability-plugin/e2e/utils'; -import { monitorDetailsPageProvider } from '../../page_objects/monitor_details'; +import { monitorDetailsPageProvider } from '../../../page_objects/uptime/monitor_details'; const dateRangeStart = '2019-09-10T12:40:08.078Z'; const dateRangeEnd = '2019-09-11T19:40:08.078Z'; diff --git a/x-pack/plugins/synthetics/e2e/journeys/monitor_details/ping_redirects.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_details/ping_redirects.journey.ts similarity index 93% rename from x-pack/plugins/synthetics/e2e/journeys/monitor_details/ping_redirects.journey.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_details/ping_redirects.journey.ts index 8917ee5418a03..b18d9183274d8 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/monitor_details/ping_redirects.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_details/ping_redirects.journey.ts @@ -7,8 +7,8 @@ import { journey, step, expect, before, Page } from '@elastic/synthetics'; import { byTestId, delay } from '@kbn/observability-plugin/e2e/utils'; -import { makeChecksWithStatus } from '../../helpers/make_checks'; -import { monitorDetailsPageProvider } from '../../page_objects/monitor_details'; +import { makeChecksWithStatus } from '../../../helpers/make_checks'; +import { monitorDetailsPageProvider } from '../../../page_objects/uptime/monitor_details'; journey('MonitorPingRedirects', async ({ page, params }: { page: Page; params: any }) => { const monitorDetails = monitorDetailsPageProvider({ page, kibanaUrl: params.kibanaUrl }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/monitor_management.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_management.journey.ts similarity index 98% rename from x-pack/plugins/synthetics/e2e/journeys/monitor_management.journey.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_management.journey.ts index 7c263a9a64a7d..8074b25aa9e15 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/monitor_management.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_management.journey.ts @@ -8,8 +8,8 @@ import uuid from 'uuid'; import { journey, step, expect, after, Page } from '@elastic/synthetics'; import { byTestId } from '@kbn/observability-plugin/e2e/utils'; -import { monitorManagementPageProvider } from '../page_objects/monitor_management'; -import { DataStream } from '../../common/runtime_types/monitor_management'; +import { monitorManagementPageProvider } from '../../page_objects/uptime/monitor_management'; +import { DataStream } from '../../../common/runtime_types/monitor_management'; const customLocation = process.env.SYNTHETICS_TEST_LOCATION; diff --git a/x-pack/plugins/synthetics/e2e/journeys/monitor_management_enablement.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_management_enablement.journey.ts similarity index 94% rename from x-pack/plugins/synthetics/e2e/journeys/monitor_management_enablement.journey.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_management_enablement.journey.ts index 2d15ba620f370..780b04d2458ea 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/monitor_management_enablement.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_management_enablement.journey.ts @@ -5,7 +5,7 @@ * 2.0. */ import { journey, step, expect, after, Page } from '@elastic/synthetics'; -import { monitorManagementPageProvider } from '../page_objects/monitor_management'; +import { monitorManagementPageProvider } from '../../page_objects/uptime/monitor_management'; journey( 'Monitor Management-enablement-superuser', diff --git a/x-pack/plugins/synthetics/e2e/journeys/monitor_name.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_name.journey.ts similarity index 95% rename from x-pack/plugins/synthetics/e2e/journeys/monitor_name.journey.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_name.journey.ts index 896ef219f204c..ed96a5eab232b 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/monitor_name.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/monitor_name.journey.ts @@ -7,7 +7,7 @@ import uuid from 'uuid'; import { journey, step, expect, Page } from '@elastic/synthetics'; import { byTestId } from '@kbn/observability-plugin/e2e/utils'; -import { monitorManagementPageProvider } from '../page_objects/monitor_management'; +import { monitorManagementPageProvider } from '../../page_objects/uptime/monitor_management'; journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) => { const name = `Test monitor ${uuid.v4()}`; diff --git a/x-pack/plugins/synthetics/e2e/journeys/private_locations/add_monitor_private_location.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/private_locations/add_monitor_private_location.ts similarity index 96% rename from x-pack/plugins/synthetics/e2e/journeys/private_locations/add_monitor_private_location.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/private_locations/add_monitor_private_location.ts index 63fa0a78adfbd..3fdcf8692318d 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/private_locations/add_monitor_private_location.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/private_locations/add_monitor_private_location.ts @@ -6,7 +6,7 @@ */ import { journey, step, expect, before } from '@elastic/synthetics'; import { assertText, byTestId, TIMEOUT_60_SEC } from '@kbn/observability-plugin/e2e/utils'; -import { monitorManagementPageProvider } from '../../page_objects/monitor_management'; +import { monitorManagementPageProvider } from '../../../page_objects/uptime/monitor_management'; journey('AddPrivateLocationMonitor', async ({ page, params: { kibanaUrl } }) => { const uptime = monitorManagementPageProvider({ page, kibanaUrl }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/private_locations/index.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/private_locations/index.ts similarity index 100% rename from x-pack/plugins/synthetics/e2e/journeys/private_locations/index.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/private_locations/index.ts diff --git a/x-pack/plugins/synthetics/e2e/journeys/private_locations/manage_locations.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/private_locations/manage_locations.ts similarity index 96% rename from x-pack/plugins/synthetics/e2e/journeys/private_locations/manage_locations.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/private_locations/manage_locations.ts index 6d318d2adfc90..a28a29be15f81 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/private_locations/manage_locations.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/private_locations/manage_locations.ts @@ -6,7 +6,7 @@ */ import { journey, step, expect, before } from '@elastic/synthetics'; import { byTestId, TIMEOUT_60_SEC } from '@kbn/observability-plugin/e2e/utils'; -import { monitorManagementPageProvider } from '../../page_objects/monitor_management'; +import { monitorManagementPageProvider } from '../../../page_objects/uptime/monitor_management'; journey('ManagePrivateLocation', async ({ page, params: { kibanaUrl } }) => { const uptime = monitorManagementPageProvider({ page, kibanaUrl }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/read_only_user/index.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/read_only_user/index.ts similarity index 100% rename from x-pack/plugins/synthetics/e2e/journeys/read_only_user/index.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/read_only_user/index.ts diff --git a/x-pack/plugins/synthetics/e2e/journeys/read_only_user/monitor_management.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/read_only_user/monitor_management.ts similarity index 90% rename from x-pack/plugins/synthetics/e2e/journeys/read_only_user/monitor_management.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/read_only_user/monitor_management.ts index 96746ba56a96c..59019178f399b 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/read_only_user/monitor_management.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/read_only_user/monitor_management.ts @@ -7,7 +7,7 @@ import { expect, journey, Page, step } from '@elastic/synthetics'; import { byTestId } from '@kbn/observability-plugin/e2e/utils'; -import { monitorManagementPageProvider } from '../../page_objects/monitor_management'; +import { monitorManagementPageProvider } from '../../../page_objects/uptime/monitor_management'; journey( 'Monitor Management read only user', diff --git a/x-pack/plugins/synthetics/e2e/journeys/step_duration.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/step_duration.journey.ts similarity index 96% rename from x-pack/plugins/synthetics/e2e/journeys/step_duration.journey.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/step_duration.journey.ts index 8883ca6e47a30..103aabdd394ac 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/step_duration.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/uptime/step_duration.journey.ts @@ -6,7 +6,7 @@ */ import { journey, step, expect } from '@elastic/synthetics'; -import { loginPageProvider } from '../page_objects/login'; +import { loginPageProvider } from '../../page_objects/login'; journey('StepsDuration', async ({ page, params }) => { const login = loginPageProvider({ page }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/uptime.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/uptime/uptime.journey.ts similarity index 100% rename from x-pack/plugins/synthetics/e2e/journeys/uptime.journey.ts rename to x-pack/plugins/synthetics/e2e/journeys/uptime/uptime.journey.ts diff --git a/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx b/x-pack/plugins/synthetics/e2e/page_objects/synthetics/synthetics_app.tsx similarity index 98% rename from x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx rename to x-pack/plugins/synthetics/e2e/page_objects/synthetics/synthetics_app.tsx index ed1feb153dffd..5f4f96ad470ec 100644 --- a/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx +++ b/x-pack/plugins/synthetics/e2e/page_objects/synthetics/synthetics_app.tsx @@ -5,9 +5,9 @@ * 2.0. */ import { expect, Page } from '@elastic/synthetics'; -import { FormMonitorType } from '../../common/runtime_types/monitor_management'; -import { loginPageProvider } from './login'; -import { utilsPageProvider } from './utils'; +import { FormMonitorType } from '../../../common/runtime_types/monitor_management'; +import { loginPageProvider } from '../login'; +import { utilsPageProvider } from '../utils'; const SIXTY_SEC_TIMEOUT = { timeout: 60 * 1000, diff --git a/x-pack/plugins/synthetics/e2e/page_objects/monitor_details.tsx b/x-pack/plugins/synthetics/e2e/page_objects/uptime/monitor_details.tsx similarity index 100% rename from x-pack/plugins/synthetics/e2e/page_objects/monitor_details.tsx rename to x-pack/plugins/synthetics/e2e/page_objects/uptime/monitor_details.tsx diff --git a/x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx b/x-pack/plugins/synthetics/e2e/page_objects/uptime/monitor_management.tsx similarity index 98% rename from x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx rename to x-pack/plugins/synthetics/e2e/page_objects/uptime/monitor_management.tsx index 540168e9917b7..4d89150143344 100644 --- a/x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx +++ b/x-pack/plugins/synthetics/e2e/page_objects/uptime/monitor_management.tsx @@ -6,9 +6,9 @@ */ import { expect, Page } from '@elastic/synthetics'; import { getQuerystring, TIMEOUT_60_SEC } from '@kbn/observability-plugin/e2e/utils'; -import { DataStream } from '../../common/runtime_types/monitor_management'; -import { loginPageProvider } from './login'; -import { utilsPageProvider } from './utils'; +import { DataStream } from '../../../common/runtime_types/monitor_management'; +import { loginPageProvider } from '../login'; +import { utilsPageProvider } from '../utils'; export function monitorManagementPageProvider({ page, diff --git a/x-pack/plugins/synthetics/e2e/page_objects/settings.tsx b/x-pack/plugins/synthetics/e2e/page_objects/uptime/settings.tsx similarity index 93% rename from x-pack/plugins/synthetics/e2e/page_objects/settings.tsx rename to x-pack/plugins/synthetics/e2e/page_objects/uptime/settings.tsx index df10f0eb71041..16e1e59f85a5b 100644 --- a/x-pack/plugins/synthetics/e2e/page_objects/settings.tsx +++ b/x-pack/plugins/synthetics/e2e/page_objects/uptime/settings.tsx @@ -7,8 +7,8 @@ import { expect, Page } from '@elastic/synthetics'; import { byTestId } from '@kbn/observability-plugin/e2e/utils'; -import { loginPageProvider } from './login'; -import { utilsPageProvider } from './utils'; +import { loginPageProvider } from '../login'; +import { utilsPageProvider } from '../utils'; export function settingsPageProvider({ page }: { page: Page; kibanaUrl: string }) { return { From 42006836a105febf482089860113c26d39c9945d Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Mon, 19 Dec 2022 12:52:19 -0500 Subject: [PATCH 23/55] [Security Solution] [Exceptions] fixes error where validation of isAlertsLoading was missing reference to memoized data view id (#147641) ## Summary ref: https://github.com/elastic/kibana/issues/147591 Thanks @nkhristinin for pairing up on this today! ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Co-authored-by: Gloria Hornero --- .../add_edit_exception_data_view.cy.ts | 57 ++++++++++++++++++- .../cypress/screens/common/page.ts | 2 + .../security_solution/cypress/tasks/alerts.ts | 17 +++--- .../cypress/tasks/exceptions.ts | 11 ++-- .../timeline_actions/alert_context_menu.tsx | 2 +- 5 files changed, 71 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts index b3c807fdf7297..79f9945219dc2 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts @@ -5,15 +5,24 @@ * 2.0. */ +import { LOADING_INDICATOR } from '../../../screens/security_header'; import { getNewRule } from '../../../objects/rule'; import { ALERTS_COUNT, EMPTY_ALERT_TABLE, NUMBER_OF_ALERTS } from '../../../screens/alerts'; import { createCustomRuleEnabled } from '../../../tasks/api_calls/rules'; import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; -import { goToClosedAlerts, goToOpenedAlerts } from '../../../tasks/alerts'; import { + addExceptionFromFirstAlert, + goToClosedAlerts, + goToOpenedAlerts, +} from '../../../tasks/alerts'; +import { + addExceptionConditions, + addExceptionFlyoutItemName, editException, editExceptionFlyoutItemName, + selectBulkCloseAlerts, submitEditedExceptionItem, + submitNewExceptionItem, } from '../../../tasks/exceptions'; import { esArchiverLoad, @@ -75,14 +84,57 @@ describe('Add exception using data views from rule details', () => { ); visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); goToRuleDetails(); - goToExceptionsTab(); + waitForAlertsToPopulate(); }); afterEach(() => { esArchiverUnload('exceptions_2'); }); + it('Creates an exception item from alert actions overflow menu', () => { + cy.get(LOADING_INDICATOR).should('not.exist'); + addExceptionFromFirstAlert(); + addExceptionFlyoutItemName(ITEM_NAME); + addExceptionConditions({ + field: 'agent.name', + operator: 'is', + values: ['foo'], + }); + selectBulkCloseAlerts(); + submitNewExceptionItem(); + + // Alerts table should now be empty from having added exception and closed + // matching alert + cy.get(EMPTY_ALERT_TABLE).should('exist'); + + // Closed alert should appear in table + goToClosedAlerts(); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + + // Remove the exception and load an event that would have matched that exception + // to show that said exception now starts to show up again + goToExceptionsTab(); + + // when removing exception and again, no more exist, empty screen shows again + removeException(); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); + + // load more docs + esArchiverLoad('exceptions_2'); + + // now that there are no more exceptions, the docs should match and populate alerts + goToAlertsTab(); + goToOpenedAlerts(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', '2 alerts'); + }); + it('Creates an exception item', () => { + goToExceptionsTab(); // when no exceptions exist, empty component shows with action to add exception cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); @@ -136,6 +188,7 @@ describe('Add exception using data views from rule details', () => { const ITEM_FIELD = 'unique_value.test'; const FIELD_DIFFERENT_FROM_EXISTING_ITEM_FIELD = 'agent.name'; + goToExceptionsTab(); // add item to edit addFirstExceptionFromRuleDetails( { diff --git a/x-pack/plugins/security_solution/cypress/screens/common/page.ts b/x-pack/plugins/security_solution/cypress/screens/common/page.ts index 3f6a130ca3314..f81d5543b830f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/common/page.ts +++ b/x-pack/plugins/security_solution/cypress/screens/common/page.ts @@ -8,3 +8,5 @@ export const PAGE_TITLE = '[data-test-subj="header-page-title"]'; export const NOT_FOUND = '[data-test-subj="notFoundPage"]'; + +export const LOADING_SPINNER = '.euiLoadingSpinner'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index 8003f1ba3c304..1e7b3047edaa2 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -46,15 +46,14 @@ import { USER_DETAILS_LINK, } from '../screens/alerts_details'; import { FIELD_INPUT } from '../screens/exceptions'; +import { LOADING_SPINNER } from '../screens/common/page'; export const addExceptionFromFirstAlert = () => { - cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true }); - cy.root() - .pipe(($el) => { - $el.find(ADD_EXCEPTION_BTN).trigger('click'); - return $el.find(FIELD_INPUT); - }) - .should('be.visible'); + expandFirstAlertActions(); + cy.get(ADD_EXCEPTION_BTN, { timeout: 10000 }).should('be.visible'); + cy.get(ADD_EXCEPTION_BTN, { timeout: 10000 }).first().click(); + cy.get(LOADING_SPINNER).should('exist'); + cy.get(LOADING_SPINNER).should('not.exist'); }; export const openAddEndpointExceptionFromFirstAlert = () => { @@ -108,8 +107,8 @@ export const closeAlerts = () => { }; export const expandFirstAlertActions = () => { - cy.get(TIMELINE_CONTEXT_MENU_BTN).should('be.visible'); - cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true }); + cy.get(TIMELINE_CONTEXT_MENU_BTN, { timeout: 10000 }).should('be.visible'); + cy.get(TIMELINE_CONTEXT_MENU_BTN, { timeout: 10000 }).first().click({ force: true }); }; export const expandFirstAlert = () => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts b/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts index ac7e5572cc1d1..3fb544101f177 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts @@ -69,12 +69,10 @@ export const editException = (updatedField: string, itemIndex = 0, fieldIndex = }; export const addExceptionFlyoutItemName = (name: string) => { - cy.root() - .pipe(($el) => { - return $el.find(EXCEPTION_ITEM_NAME_INPUT); - }) - .type(`${name}{enter}`) - .should('have.value', name); + cy.get(EXCEPTION_ITEM_NAME_INPUT).should('exist'); + cy.get(EXCEPTION_ITEM_NAME_INPUT).should('be.visible'); + cy.get(EXCEPTION_ITEM_NAME_INPUT).first().focus(); + cy.get(EXCEPTION_ITEM_NAME_INPUT).type(`${name}{enter}`).should('have.value', name); }; export const editExceptionFlyoutItemName = (name: string) => { @@ -88,6 +86,7 @@ export const editExceptionFlyoutItemName = (name: string) => { }; export const selectBulkCloseAlerts = () => { + cy.get(CLOSE_ALERTS_CHECKBOX).should('exist'); cy.get(CLOSE_ALERTS_CHECKBOX).click({ force: true }); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 82c02351a8cf4..c5ecfe797bc36 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -425,7 +425,7 @@ export const AddExceptionFlyoutWrapper: React.FC const isLoading = (isLoadingAlertData && isSignalIndexLoading) || enrichedAlert == null || - memoRuleIndices == null; + (memoRuleIndices == null && memoDataViewId == null); return ( Date: Mon, 19 Dec 2022 13:27:11 -0500 Subject: [PATCH 24/55] [Synthetics] do not show api key error unless there are monitors (#147783) ## Summary Resolves https://github.com/elastic/kibana/issues/147586 Do not show errors related to missing synthetics api key unless there are synthetics monitors present. ### Testing 1. Create a brand new cluster via `oblt-cli` 2. Add that cluster details to your `kibana.dev.yml` file 3. Monitor your Kibana logs. Ensure there is not an error log complaining about the api key. For example: ``` [2022-12-14T20:19:43.516-05:00][ERROR][plugins.synthetics] API key is not valid. Cannot push monitor configuration to synthetics public testing locations ``` --- .../synthetics/server/synthetics_service/synthetics_service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts index 2ae92fa4928bf..f50ab8d0c35c1 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts @@ -294,7 +294,6 @@ export class SyntheticsService { async pushConfigs() { const service = this; const subject = new Subject(); - const output = await this.getOutput(); subject.subscribe(async (monitorConfigs) => { try { @@ -305,6 +304,8 @@ export class SyntheticsService { return null; } + const output = await this.getOutput(); + if (!output) { sendErrorTelemetryEvents(service.logger, service.server.telemetry, { reason: 'API key is not valid.', From b5d3a635169710968cfcdbefd12f34548cdd1d15 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 19 Dec 2022 13:44:39 -0500 Subject: [PATCH 25/55] [controls] add filters, query, and timeRange props to ControlGroupRenderer and create search example (#147581) Part of https://github.com/elastic/kibana/issues/145428 PR makes the following changes: 1) updates ControlGroupRenderer component with declarative properties for filters, query, and timeRange. 2) creates a search example showing how to use controls to narrow results 3) Updates redux example to use web logs sample data set 4) Updates existing uses of ControlGroupRenderer to use new props. Screen Shot 2022-12-14 at 4 29 58 PM Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- examples/controls_example/kibana.json | 9 +- examples/controls_example/public/app.tsx | 40 +++-- .../public/basic_redux_example.tsx | 87 ++++----- examples/controls_example/public/constants.ts | 9 + examples/controls_example/public/plugin.tsx | 9 +- .../public/search_example.tsx | 168 ++++++++++++++++++ examples/controls_example/tsconfig.json | 1 + .../control_group/control_group_renderer.tsx | 31 +++- .../hosts/components/controls_content.tsx | 41 ++--- .../timeslider/timeslider.tsx | 18 +- 10 files changed, 299 insertions(+), 114 deletions(-) create mode 100644 examples/controls_example/public/constants.ts create mode 100644 examples/controls_example/public/search_example.tsx diff --git a/examples/controls_example/kibana.json b/examples/controls_example/kibana.json index 88dd37f41dcfa..605714954967d 100644 --- a/examples/controls_example/kibana.json +++ b/examples/controls_example/kibana.json @@ -7,5 +7,12 @@ "version": "1.0.0", "kibanaVersion": "kibana", "ui": true, - "requiredPlugins": ["data", "developerExamples", "presentationUtil", "controls"] + "requiredPlugins": [ + "controls", + "data", + "developerExamples", + "embeddable", + "navigation", + "presentationUtil" + ] } diff --git a/examples/controls_example/public/app.tsx b/examples/controls_example/public/app.tsx index 501d48af70656..e33ea69a0ef65 100644 --- a/examples/controls_example/public/app.tsx +++ b/examples/controls_example/public/app.tsx @@ -8,34 +8,36 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { EuiSpacer } from '@elastic/eui'; import { AppMountParameters } from '@kbn/core/public'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { ControlsExampleStartDeps } from './plugin'; import { BasicReduxExample } from './basic_redux_example'; +import { SearchExample } from './search_example'; -const ControlsExamples = ({ dataViewId }: { dataViewId?: string }) => { - const examples = dataViewId ? ( - <> - - - ) : ( -
      {'Please install e-commerce sample data to run controls examples.'}
      - ); - return ( +export const renderApp = async ( + { data, navigation }: ControlsExampleStartDeps, + { element }: AppMountParameters +) => { + const dataViews = await data.dataViews.find('kibana_sample_data_logs'); + const examples = + dataViews.length > 0 ? ( + <> + + + + + ) : ( +
      {'Install web logs sample data to run controls examples.'}
      + ); + + ReactDOM.render( {examples} - + , + element ); -}; - -export const renderApp = async ( - { data }: ControlsExampleStartDeps, - { element }: AppMountParameters -) => { - const dataViews = await data.dataViews.find('kibana_sample_data_ecommerce'); - const dataViewId = dataViews.length > 0 ? dataViews[0].id : undefined; - ReactDOM.render(, element); return () => ReactDOM.unmountComponentAtNode(element); }; diff --git a/examples/controls_example/public/basic_redux_example.tsx b/examples/controls_example/public/basic_redux_example.tsx index 03edcd82b71a2..0586882d251b2 100644 --- a/examples/controls_example/public/basic_redux_example.tsx +++ b/examples/controls_example/public/basic_redux_example.tsx @@ -15,15 +15,8 @@ import { ControlStyle, } from '@kbn/controls-plugin/public'; import { withSuspense } from '@kbn/presentation-util-plugin/public'; -import { - EuiButtonGroup, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; +import { EuiButtonGroup, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { ViewMode } from '@kbn/embeddable-plugin/public'; const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer); @@ -44,51 +37,36 @@ export const BasicReduxExample = ({ dataViewId }: { dataViewId: string }) => { const controlStyle = select((state) => state.explicitInput.controlStyle); return ( - <> - - - -

      Choose a style for your control group:

      -
      -
      - - { - dispatch(setControlStyle(value)); - }} - type="single" - /> - -
      - - + { + dispatch(setControlStyle(value)); + }} + type="single" + /> ); }; return ( <> -

      Basic Redux Example

      +

      Redux example

      -

      - This example uses the redux context from the control group container in order to - dynamically change the style of the control group. -

      +

      Use the redux context from the control group to set layout style.

      @@ -105,17 +83,22 @@ export const BasicReduxExample = ({ dataViewId }: { dataViewId: string }) => { getInitialInput={async (initialInput, builder) => { await builder.addDataControlFromField(initialInput, { dataViewId, - fieldName: 'customer_first_name.keyword', - width: 'small', + title: 'Destintion country', + fieldName: 'geo.dest', + width: 'medium', + grow: false, }); await builder.addDataControlFromField(initialInput, { dataViewId, - fieldName: 'customer_last_name.keyword', + fieldName: 'bytes', width: 'medium', - grow: false, - title: 'Last Name', + grow: true, + title: 'Bytes', }); - return initialInput; + return { + ...initialInput, + viewMode: ViewMode.VIEW, + }; }} /> diff --git a/examples/controls_example/public/constants.ts b/examples/controls_example/public/constants.ts new file mode 100644 index 0000000000000..a077239d980c2 --- /dev/null +++ b/examples/controls_example/public/constants.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const PLUGIN_ID = 'controlsExamples'; diff --git a/examples/controls_example/public/plugin.tsx b/examples/controls_example/public/plugin.tsx index 6001e9779b526..7b79d0f415d6e 100644 --- a/examples/controls_example/public/plugin.tsx +++ b/examples/controls_example/public/plugin.tsx @@ -13,9 +13,11 @@ import { CoreStart, Plugin, } from '@kbn/core/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public'; +import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; import img from './control_group_image.png'; +import { PLUGIN_ID } from './constants'; interface SetupDeps { developerExamples: DeveloperExamplesSetup; @@ -23,6 +25,7 @@ interface SetupDeps { export interface ControlsExampleStartDeps { data: DataPublicPluginStart; + navigation: NavigationPublicPluginStart; } export class ControlsExamplePlugin @@ -30,7 +33,7 @@ export class ControlsExamplePlugin { public setup(core: CoreSetup, { developerExamples }: SetupDeps) { core.application.register({ - id: 'controlsExamples', + id: PLUGIN_ID, title: 'Controls examples', navLinkStatus: AppNavLinkStatus.hidden, async mount(params: AppMountParameters) { diff --git a/examples/controls_example/public/search_example.tsx b/examples/controls_example/public/search_example.tsx new file mode 100644 index 0000000000000..0fba77fa8ddfe --- /dev/null +++ b/examples/controls_example/public/search_example.tsx @@ -0,0 +1,168 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect, useState } from 'react'; +import uuid from 'uuid/v4'; +import { lastValueFrom } from 'rxjs'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; +import type { Filter, Query, TimeRange } from '@kbn/es-query'; +import { ViewMode } from '@kbn/embeddable-plugin/public'; +import { + EuiCallOut, + EuiLoadingSpinner, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { LazyControlGroupRenderer, ControlGroupContainer } from '@kbn/controls-plugin/public'; +import { withSuspense } from '@kbn/presentation-util-plugin/public'; +import { PLUGIN_ID } from './constants'; + +const ControlGroupRenderer = withSuspense(LazyControlGroupRenderer); + +interface Props { + data: DataPublicPluginStart; + dataView: DataView; + navigation: NavigationPublicPluginStart; +} + +export const SearchExample = ({ data, dataView, navigation }: Props) => { + const [controlFilters, setControlFilters] = useState([]); + const [controlGroup, setControlGroup] = useState(); + const [hits, setHits] = useState(0); + const [filters, setFilters] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [query, setQuery] = useState({ + language: 'kuery', + query: '', + }); + const [timeRange, setTimeRange] = useState({ from: 'now-7d', to: 'now' }); + + useEffect(() => { + if (!controlGroup) { + return; + } + const subscription = controlGroup.onFiltersPublished$.subscribe((newFilters) => { + setControlFilters([...newFilters]); + }); + return () => { + subscription.unsubscribe(); + }; + }, [controlGroup]); + + useEffect(() => { + const abortController = new AbortController(); + const search = async () => { + setIsSearching(true); + const searchSource = await data.search.searchSource.create(); + searchSource.setField('index', dataView); + searchSource.setField('size', 0); + searchSource.setField('filter', [ + ...filters, + ...controlFilters, + data.query.timefilter.timefilter.createFilter(dataView, timeRange), + ] as Filter[]); + searchSource.setField('query', query); + const { rawResponse: resp } = await lastValueFrom( + searchSource.fetch$({ + abortSignal: abortController.signal, + sessionId: uuid(), + legacyHitsTotal: false, + }) + ); + const total = resp.hits?.total as undefined | { relation: string; value: number }; + if (total !== undefined) { + setHits(total.value); + } + setIsSearching(false); + }; + + search().catch((error) => { + setIsSearching(false); + if (error.name === 'AbortError') { + // ignore abort errors + } else { + // eslint-disable-next-line no-console + console.error(error); + } + }); + + return () => { + abortController.abort(); + }; + }, [controlFilters, data, dataView, filters, query, timeRange]); + + return ( + <> + +

      Search example

      +
      + +

      + Pass filters, query, and time range to narrow controls. Combine search bar filters with + controls filters to narrow results. +

      +
      + + + { + // filterManager.setFilters populates filter.meta so filter pill has pretty title + data.query.filterManager.setFilters(newFilters); + setFilters(newFilters); + }} + onQuerySubmit={({ dateRange, query: newQuery }) => { + setQuery(newQuery); + setTimeRange(dateRange); + }} + query={query} + showSearchBar={true} + /> + { + await builder.addDataControlFromField(initialInput, { + dataViewId: dataView.id!, + title: 'Destintion country', + fieldName: 'geo.dest', + width: 'medium', + grow: false, + }); + await builder.addDataControlFromField(initialInput, { + dataViewId: dataView.id!, + fieldName: 'bytes', + width: 'medium', + grow: true, + title: 'Bytes', + }); + return { + ...initialInput, + viewMode: ViewMode.VIEW, + }; + }} + onLoadComplete={async (newControlGroup) => { + setControlGroup(newControlGroup); + }} + query={query} + timeRange={timeRange} + /> + + {isSearching ? :

      Hits: {hits}

      } +
      +
      + + ); +}; diff --git a/examples/controls_example/tsconfig.json b/examples/controls_example/tsconfig.json index 1e8a32f62734e..43673c863c7d4 100644 --- a/examples/controls_example/tsconfig.json +++ b/examples/controls_example/tsconfig.json @@ -17,6 +17,7 @@ { "path": "../developer_examples/tsconfig.json" }, { "path": "../../src/plugins/data/tsconfig.json" }, { "path": "../../src/plugins/controls/tsconfig.json" }, + { "path": "../../src/plugins/navigation/tsconfig.json" }, { "path": "../../src/plugins/presentation_util/tsconfig.json" } ] } diff --git a/src/plugins/controls/public/control_group/control_group_renderer.tsx b/src/plugins/controls/public/control_group/control_group_renderer.tsx index 987c8fcd7e5e5..9611c5a3c6c47 100644 --- a/src/plugins/controls/public/control_group/control_group_renderer.tsx +++ b/src/plugins/controls/public/control_group/control_group_renderer.tsx @@ -7,11 +7,14 @@ */ import uuid from 'uuid'; +import { isEqual } from 'lodash'; import useLifecycles from 'react-use/lib/useLifecycles'; -import React, { useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { IEmbeddable } from '@kbn/embeddable-plugin/public'; import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public'; +import type { Filter, TimeRange, Query } from '@kbn/es-query'; +import { compareFilters } from '@kbn/es-query'; import { pluginServices } from '../services'; import { getDefaultControlGroupInput } from '../../common'; @@ -26,16 +29,22 @@ import { controlGroupReducers } from './state/control_group_reducers'; import { controlGroupInputBuilder } from './control_group_input_builder'; export interface ControlGroupRendererProps { - onLoadComplete?: (controlGroup: ControlGroupContainer) => void; + filters?: Filter[]; getInitialInput: ( initialInput: Partial, builder: typeof controlGroupInputBuilder ) => Promise>; + onLoadComplete?: (controlGroup: ControlGroupContainer) => void; + timeRange?: TimeRange; + query?: Query; } export const ControlGroupRenderer = ({ onLoadComplete, getInitialInput, + filters, + timeRange, + query, }: ControlGroupRendererProps) => { const controlGroupRef = useRef(null); const [controlGroup, setControlGroup] = useState(); @@ -74,6 +83,24 @@ export const ControlGroupRenderer = ({ } ); + useEffect(() => { + if (!controlGroup) { + return; + } + + if ( + (timeRange && !isEqual(controlGroup.getInput().timeRange, timeRange)) || + !compareFilters(controlGroup.getInput().filters ?? [], filters ?? []) || + !isEqual(controlGroup.getInput().query, query) + ) { + controlGroup.updateInput({ + timeRange, + query, + filters, + }); + } + }, [query, filters, controlGroup, timeRange]); + return
      ; }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx index 085330abf979a..d3f2df19e78c5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx @@ -8,8 +8,7 @@ import React, { useEffect, useState } from 'react'; import { ControlGroupContainer, CONTROL_GROUP_TYPE } from '@kbn/controls-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; -import { Filter, TimeRange, compareFilters } from '@kbn/es-query'; -import { isEqual } from 'lodash'; +import { Filter, TimeRange } from '@kbn/es-query'; import { LazyControlsRenderer } from './lazy_controls_renderer'; import { useControlPanels } from '../hooks/use_control_panels_url_state'; @@ -44,21 +43,26 @@ export const ControlsContent: React.FC = ({ if (!controlGroup) { return; } - if ( - !isEqual(controlGroup.getInput().timeRange, timeRange) || - !compareFilters(controlGroup.getInput().filters ?? [], filters) || - !isEqual(controlGroup.getInput().query, query) - ) { - controlGroup.updateInput({ - timeRange, - query, - filters, + const filtersSubscription = controlGroup.onFiltersPublished$.subscribe((newFilters) => { + setPanelFilters([...newFilters]); + }); + const inputSubscription = controlGroup + .getInput$() + .subscribe(({ panels, filters: currentFilters }) => { + setControlPanels(panels); + if (currentFilters?.length === 0) { + setPanelFilters([]); + } }); - } - }, [query, filters, controlGroup, timeRange]); + return () => { + filtersSubscription.unsubscribe(); + inputSubscription.unsubscribe(); + }; + }, [controlGroup, setControlPanels, setPanelFilters]); return ( ({ id: dataViewId, type: CONTROL_GROUP_TYPE, @@ -74,16 +78,9 @@ export const ControlsContent: React.FC = ({ })} onLoadComplete={(newControlGroup) => { setControlGroup(newControlGroup); - newControlGroup.onFiltersPublished$.subscribe((newFilters) => { - setPanelFilters([...newFilters]); - }); - newControlGroup.getInput$().subscribe(({ panels, filters: currentFilters }) => { - setControlPanels(panels); - if (currentFilters?.length === 0) { - setPanelFilters([]); - } - }); }} + query={query} + timeRange={timeRange} /> ); }; diff --git a/x-pack/plugins/maps/public/connected_components/timeslider/timeslider.tsx b/x-pack/plugins/maps/public/connected_components/timeslider/timeslider.tsx index 75e474cc064fc..a0756e35b4323 100644 --- a/x-pack/plugins/maps/public/connected_components/timeslider/timeslider.tsx +++ b/x-pack/plugins/maps/public/connected_components/timeslider/timeslider.tsx @@ -31,7 +31,6 @@ export interface Props { export class Timeslider extends Component { private _isMounted: boolean = false; - private _controlGroup?: ControlGroupContainer | undefined; private readonly _subscriptions = new Subscription(); componentWillUnmount() { @@ -39,17 +38,6 @@ export class Timeslider extends Component { this._subscriptions.unsubscribe(); } - componentDidUpdate() { - if ( - this._controlGroup && - !_.isEqual(this._controlGroup.getInput().timeRange, this.props.timeRange) - ) { - this._controlGroup.updateInput({ - timeRange: this.props.timeRange, - }); - } - } - componentDidMount() { this._isMounted = true; } @@ -71,9 +59,8 @@ export class Timeslider extends Component { return; } - this._controlGroup = controlGroup; this._subscriptions.add( - this._controlGroup + controlGroup .getOutput$() .pipe( distinctUntilChanged(({ timeslice: timesliceA }, { timeslice: timesliceB }) => @@ -84,7 +71,7 @@ export class Timeslider extends Component { // use waitForTimesliceToLoad$ observable to wait until next frame loaded // .pipe(first()) waits until the first value is emitted from an observable and then automatically unsubscribes this.props.waitForTimesliceToLoad$.pipe(first()).subscribe(() => { - this._controlGroup!.anyControlOutputConsumerLoading$.next(false); + controlGroup.anyControlOutputConsumerLoading$.next(false); }); this.props.setTimeslice( @@ -105,6 +92,7 @@ export class Timeslider extends Component {
      ); From 800f45180cb1706a67f264ba5cee95c1a2031b8a Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 19 Dec 2022 20:40:18 +0100 Subject: [PATCH 26/55] [@kbn/handlebars] Ensure only decorators have an `options.args` property (#147791) After adding support for decorators an `args` property has added to `HelperOptions` (which is shared with decorators). To not leak this into regular helpers and to align with the upstream handlebars, this PR removes the `args` property from regular helpers so that it's only visible to decorators. --- packages/kbn-handlebars/index.test.ts | 145 +++++++++++++++++++------- packages/kbn-handlebars/index.ts | 49 +++++---- 2 files changed, 135 insertions(+), 59 deletions(-) diff --git a/packages/kbn-handlebars/index.test.ts b/packages/kbn-handlebars/index.test.ts index 9d255bf676746..82f837a6b333f 100644 --- a/packages/kbn-handlebars/index.test.ts +++ b/packages/kbn-handlebars/index.test.ts @@ -49,51 +49,118 @@ Expecting 'ID', 'STRING', 'NUMBER', 'BOOLEAN', 'UNDEFINED', 'NULL', 'DATA', got } }); -it('Only provide options.fn/inverse to block helpers', () => { - function toHaveProperties(...args: any[]) { - toHaveProperties.calls++; - const options = args[args.length - 1]; - expect(options).toHaveProperty('fn'); - expect(options).toHaveProperty('inverse'); - return 42; - } - toHaveProperties.calls = 0; - - function toNotHaveProperties(...args: any[]) { - toNotHaveProperties.calls++; - const options = args[args.length - 1]; - expect(options).not.toHaveProperty('fn'); - expect(options).not.toHaveProperty('inverse'); - return 42; - } - toNotHaveProperties.calls = 0; +// Extra "helpers" tests +describe('helpers', () => { + it('Only provide options.fn/inverse to block helpers', () => { + function toHaveProperties(...args: any[]) { + toHaveProperties.calls++; + const options = args[args.length - 1]; + expect(options).toHaveProperty('fn'); + expect(options).toHaveProperty('inverse'); + return 42; + } + toHaveProperties.calls = 0; + + function toNotHaveProperties(...args: any[]) { + toNotHaveProperties.calls++; + const options = args[args.length - 1]; + expect(options).not.toHaveProperty('fn'); + expect(options).not.toHaveProperty('inverse'); + return 42; + } + toNotHaveProperties.calls = 0; + + const nonBlockTemplates = ['{{foo}}', '{{foo 1 2}}']; + const blockTemplates = ['{{#foo}}42{{/foo}}', '{{#foo 1 2}}42{{/foo}}']; + + for (const template of nonBlockTemplates) { + expectTemplate(template) + .withInput({ + foo: toNotHaveProperties, + }) + .toCompileTo('42'); - const nonBlockTemplates = ['{{foo}}', '{{foo 1 2}}']; - const blockTemplates = ['{{#foo}}42{{/foo}}', '{{#foo 1 2}}42{{/foo}}']; + expectTemplate(template).withHelper('foo', toNotHaveProperties).toCompileTo('42'); + } - for (const template of nonBlockTemplates) { - expectTemplate(template) - .withInput({ - foo: toNotHaveProperties, - }) - .toCompileTo('42'); + for (const template of blockTemplates) { + expectTemplate(template) + .withInput({ + foo: toHaveProperties, + }) + .toCompileTo('42'); - expectTemplate(template).withHelper('foo', toNotHaveProperties).toCompileTo('42'); - } + expectTemplate(template).withHelper('foo', toHaveProperties).toCompileTo('42'); + } + + const factor = process.env.AST || process.env.EVAL ? 1 : 2; + expect(toNotHaveProperties.calls).toEqual(nonBlockTemplates.length * 2 * factor); + expect(toHaveProperties.calls).toEqual(blockTemplates.length * 2 * factor); + }); - for (const template of blockTemplates) { - expectTemplate(template) + it('should pass expected "this" and arguments to helper functions', () => { + expectTemplate('{{hello "world" 12 true false}}') + .withHelper('hello', function (this: any, ...args) { + expect(this).toMatchInlineSnapshot(` + Object { + "people": Array [ + Object { + "id": 1, + "name": "Alan", + }, + Object { + "id": 2, + "name": "Yehuda", + }, + ], + } + `); + expect(args).toMatchInlineSnapshot(` + Array [ + "world", + 12, + true, + false, + Object { + "data": Object { + "root": Object { + "people": Array [ + Object { + "id": 1, + "name": "Alan", + }, + Object { + "id": 2, + "name": "Yehuda", + }, + ], + }, + }, + "hash": Object {}, + "loc": Object { + "end": Object { + "column": 31, + "line": 1, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "lookupProperty": [Function], + "name": "hello", + }, + ] + `); + }) .withInput({ - foo: toHaveProperties, + people: [ + { name: 'Alan', id: 1 }, + { name: 'Yehuda', id: 2 }, + ], }) - .toCompileTo('42'); - - expectTemplate(template).withHelper('foo', toHaveProperties).toCompileTo('42'); - } - - const factor = process.env.AST || process.env.EVAL ? 1 : 2; - expect(toNotHaveProperties.calls).toEqual(nonBlockTemplates.length * 2 * factor); - expect(toHaveProperties.calls).toEqual(blockTemplates.length * 2 * factor); + .toCompileTo(''); + }); }); // Extra "blocks" tests diff --git a/packages/kbn-handlebars/index.ts b/packages/kbn-handlebars/index.ts index a7ad36a9e8663..9f8256a3bb516 100644 --- a/packages/kbn-handlebars/index.ts +++ b/packages/kbn-handlebars/index.ts @@ -392,17 +392,13 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { decorator: hbs.AST.DecoratorBlock | hbs.AST.Decorator, prog: Handlebars.TemplateDelegate ) { - // TypeScript: The types indicate that `decorator.path` technically can be an `hbs.AST.Literal`. However, the upstream codebase always treats it as an `hbs.AST.PathExpression`, so we do too. - const name = (decorator.path as hbs.AST.PathExpression).original; const props = {}; - // TypeScript: Because `decorator` can be of type `hbs.AST.Decorator`, TS indicates that `decorator.path` technically can be an `hbs.AST.Literal`. However, the upstream codebase always treats it as an `hbs.AST.PathExpression`, so we do too. - const options = this.setupParams(decorator as hbs.AST.DecoratorBlock, name); - // @ts-expect-error: Property 'lookupProperty' does not exist on type 'HelperOptions' - delete options.lookupProperty; // There's really no tests/documentation on this, but to match the upstream codebase we'll remove `lookupProperty` from the decorator context + const options = this.setupDecoratorOptions(decorator); const result = this.container.lookupProperty( this.container.decorators, - name + // @ts-expect-error: Property 'name' does not exist on type 'HelperOptions' - The types are wrong + options.name )(prog, props, this.container, options); Object.assign(result || prog, props); @@ -642,31 +638,44 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor { }; } - private setupParams( - node: ProcessableNodeWithPathParts, - helperName: string + private setupDecoratorOptions( + decorator: hbs.AST.Decorator | hbs.AST.DecoratorBlock ): Handlebars.HelperOptions { - const options: Handlebars.HelperOptions = { - // @ts-expect-error: Name should be on there, but the offical types doesn't know this - name: helperName, - hash: this.getHash(node), - data: this.runtimeOptions!.data, - loc: { start: node.loc.start, end: node.loc.end }, - }; + // TypeScript: The types indicate that `decorator.path` technically can be an `hbs.AST.Literal`. However, the upstream codebase always treats it as an `hbs.AST.PathExpression`, so we do too. + const name = (decorator.path as hbs.AST.PathExpression).original; + const options = this.setupParams(decorator as hbs.AST.DecoratorBlock, name); - if (node.params.length > 0) { + if (decorator.params.length > 0) { if (!this.processedRootDecorators) { // When processing the root decorators, temporarily remove the root context so it's not accessible to the decorator const context = this.scopes.shift(); // @ts-expect-error: Property 'args' does not exist on type 'HelperOptions'. The 'args' property is expected in decorators - options.args = this.resolveNodes(node.params); + options.args = this.resolveNodes(decorator.params); this.scopes.unshift(context); } else { // @ts-expect-error: Property 'args' does not exist on type 'HelperOptions'. The 'args' property is expected in decorators - options.args = this.resolveNodes(node.params); + options.args = this.resolveNodes(decorator.params); } } + // @ts-expect-error: Property 'lookupProperty' does not exist on type 'HelperOptions' + delete options.lookupProperty; // There's really no tests/documentation on this, but to match the upstream codebase we'll remove `lookupProperty` from the decorator context + + return options; + } + + private setupParams( + node: ProcessableNodeWithPathParts, + helperName: string + ): Handlebars.HelperOptions { + const options: Handlebars.HelperOptions = { + // @ts-expect-error: Name should be on there, but the offical types doesn't know this + name: helperName, + hash: this.getHash(node), + data: this.runtimeOptions!.data, + loc: { start: node.loc.start, end: node.loc.end }, + }; + if (isBlock(node)) { options.fn = this.generateProgramFunction(node.program); if (node.program) this.processDecorators(node.program, options.fn); From 88733fc48f2723569d72c59141788b201e777ddd Mon Sep 17 00:00:00 2001 From: Jeramy Soucy Date: Mon, 19 Dec 2022 15:00:29 -0500 Subject: [PATCH 27/55] [Saved Objects] Consolidates Check & Enforce Authz Extension Methods (#147287) Resolves #147045 Combines the Saved Objects Security Extension's Check Authorization and Enforce Authorization methods into a single Perform Authorization method to simplify usage and prepare for migration of audit & authorization logic from the Saved Objects Repository to the Security Extension. ## Follow-on Work: - https://github.com/elastic/kibana/issues/147048 - https://github.com/elastic/kibana/issues/147049 ## Testing ### Unit Tests [ ] repository.security_extension.test.ts [ ] repository.spaces_extension.test.ts [ ] collect_multi_namespace_references.test.ts [ ] internal_bulk_resolve.test.ts [ ] update_objects_spaces.test.ts [ ] saved_objects_security_extension.test.ts [ ] secure_spaces_client_wrapper.test.ts Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- ...collect_multi_namespace_references.test.ts | 96 +- .../lib/collect_multi_namespace_references.ts | 18 +- .../src/lib/internal_bulk_resolve.test.ts | 76 +- .../src/lib/internal_bulk_resolve.ts | 10 +- .../lib/repository.security_extension.test.ts | 897 +++++++----------- .../lib/repository.spaces_extension.test.ts | 4 +- .../src/lib/repository.ts | 308 +++--- .../src/lib/update_objects_spaces.test.ts | 114 ++- .../src/lib/update_objects_spaces.ts | 40 +- .../mocks/saved_objects_extensions.mock.ts | 2 +- .../test_helpers/repository.test.common.ts | 73 +- .../src/saved_objects_extensions.mock.ts | 2 +- .../core-saved-objects-server/index.ts | 1 + .../src/extensions/security.ts | 45 +- .../core-saved-objects-utils-server/index.ts | 2 + .../src/saved_objects_test_utils.test.ts | 160 ++++ .../src/saved_objects_test_utils.ts | 70 ++ src/core/server/index.ts | 2 +- .../saved_objects_security_extension.test.ts | 557 ++++++----- .../saved_objects_security_extension.ts | 28 +- .../secure_spaces_client_wrapper.test.ts | 49 +- .../spaces/secure_spaces_client_wrapper.ts | 15 +- 22 files changed, 1328 insertions(+), 1241 deletions(-) create mode 100644 packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.test.ts create mode 100644 packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.ts diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.ts index 55080f8e9e4e2..c9529a6828d7a 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.test.ts @@ -17,7 +17,11 @@ import type { SavedObjectsCollectMultiNamespaceReferencesObject, SavedObjectsCollectMultiNamespaceReferencesOptions, } from '@kbn/core-saved-objects-api-server'; -import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-utils-server'; +import { + setMapsAreEqual, + SavedObjectsErrorHelpers, + setsAreEqual, +} from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsSerializer } from '@kbn/core-saved-objects-base-server-internal'; import { typeRegistryMock } from '@kbn/core-saved-objects-base-server-mocks'; import { @@ -31,12 +35,8 @@ import { AuditAction, type ISavedObjectsSecurityExtension } from '@kbn/core-save import { authMap, enforceError, - typeMapsAreEqual, - setsAreEqual, - setupCheckAuthorized, - setupCheckUnauthorized, - setupEnforceFailure, - setupEnforceSuccess, + setupPerformAuthFullyAuthorized, + setupPerformAuthEnforceFailure, setupRedactPassthrough, } from '../test_helpers/repository.test.common'; import { savedObjectsExtensionsMock } from '../mocks/saved_objects_extensions.mock'; @@ -487,7 +487,7 @@ describe('collectMultiNamespaceReferences', () => { }); afterEach(() => { - mockSecurityExt.checkAuthorization.mockReset(); + mockSecurityExt.performAuthorization.mockReset(); mockSecurityExt.enforceAuthorization.mockReset(); mockSecurityExt.redactNamespaces.mockReset(); mockSecurityExt.addAuditEvent.mockReset(); @@ -495,25 +495,21 @@ describe('collectMultiNamespaceReferences', () => { describe(`errors`, () => { test(`propagates decorated error when not authorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); // Unlike other functions, it doesn't validate the level of authorization first, so we need to // carry on and mock the enforce function as well to create an unauthorized condition - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect(collectMultiNamespaceReferences(params)).rejects.toThrow(enforceError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`adds audit event per object when not successful`, async () => { - setupCheckUnauthorized(mockSecurityExt); // Unlike other functions, it doesn't validate the level of authorization first, so we need to // carry on and mock the enforce function as well to create an unauthorized condition - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect(collectMultiNamespaceReferences(params)).rejects.toThrow(enforceError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(objects.length); objects.forEach((obj) => { @@ -528,23 +524,19 @@ describe('collectMultiNamespaceReferences', () => { describe('checks privileges', () => { beforeEach(() => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); }); test(`in the default state`, async () => { await expect(collectMultiNamespaceReferences(params)).rejects.toThrow(enforceError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedSpaces = new Set(['default', ...SPACES, ...obj1LegacySpaces]); - const { spaces: actualSpaces } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; - expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); + const expectedEnforceMap = new Map([[objects[0].type, new Set(['default'])]]); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - const expectedTypesAndSpaces = new Map([[objects[0].type, new Set(['default'])]]); - const { typesAndSpaces: actualTypesAndSpaces } = - mockSecurityExt.enforceAuthorization.mock.calls[0][0]; - - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); + const { spaces: actualSpaces, enforceMap: actualEnforceMap } = + mockSecurityExt.performAuthorization.mock.calls[0][0]; + expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); }); test(`in a non-default state`, async () => { @@ -553,17 +545,13 @@ describe('collectMultiNamespaceReferences', () => { collectMultiNamespaceReferences({ ...params, options: { namespace } }) ).rejects.toThrow(enforceError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedSpaces = new Set([namespace, ...SPACES, ...obj1LegacySpaces]); - const { spaces: actualSpaces } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + const expectedEnforceMap = new Map([[objects[0].type, new Set([namespace])]]); + const { spaces: actualSpaces, enforceMap: actualEnforceMap } = + mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); - - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - const expectedTypesAndSpaces = new Map([[objects[0].type, new Set([namespace])]]); - const { typesAndSpaces: actualTypesAndSpaces } = - mockSecurityExt.enforceAuthorization.mock.calls[0][0]; - - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); }); test(`with purpose 'collectMultiNamespaceReferences'`, async () => { @@ -571,19 +559,17 @@ describe('collectMultiNamespaceReferences', () => { purpose: 'collectMultiNamespaceReferences', }; - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect(collectMultiNamespaceReferences({ ...params, options })).rejects.toThrow( enforceError ); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.checkAuthorization).toBeCalledWith( + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toBeCalledWith( expect.objectContaining({ actions: new Set(['bulk_get']), }) ); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); }); test(`with purpose 'updateObjectsSpaces'`, async () => { @@ -591,45 +577,40 @@ describe('collectMultiNamespaceReferences', () => { purpose: 'updateObjectsSpaces', }; - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect(collectMultiNamespaceReferences({ ...params, options })).rejects.toThrow( enforceError ); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.checkAuthorization).toBeCalledWith( + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toBeCalledWith( expect.objectContaining({ actions: new Set(['share_to_space']), }) ); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); }); }); describe('success', () => { beforeEach(async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); await collectMultiNamespaceReferences(params); }); test(`calls redactNamespaces with type, spaces, and authorization map`, async () => { - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedSpaces = new Set(['default', ...SPACES, ...obj1LegacySpaces]); - const { spaces: actualSpaces } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + const { spaces: actualSpaces } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); const resultObjects = [obj1, obj2, obj3]; // enforce is called once for all objects/spaces, then once per object - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes( - 1 + resultObjects.length - ); + expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(resultObjects.length); const expectedTypesAndSpaces = new Map([[objects[0].type, new Set(['default'])]]); const { typesAndSpaces: actualTypesAndSpaces } = mockSecurityExt.enforceAuthorization.mock.calls[0][0]; - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); + expect(setMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); // Redact is called once per object, but an additional time for object 1 because it has legacy URL aliases in another set of spaces expect(mockSecurityExt.redactNamespaces).toBeCalledTimes(resultObjects.length + 1); @@ -653,15 +634,10 @@ describe('collectMultiNamespaceReferences', () => { }); test(`adds audit event per object when successful`, async () => { - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const resultObjects = [obj1, obj2, obj3]; - // enforce is called once for all objects/spaces, then once per object - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes( - 1 + resultObjects.length - ); - expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(resultObjects.length); resultObjects.forEach((obj) => { expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({ diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts index ffe3950394dd8..9a97f2f91e4eb 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/collect_multi_namespace_references.ts @@ -252,21 +252,18 @@ async function optionallyUseSecurity( } const action = purpose === 'updateObjectsSpaces' ? ('share_to_space' as const) : ('bulk_get' as const); - const { typeMap } = await securityExtension.checkAuthorization({ - types: typesToAuthorize, - spaces: spacesToAuthorize, - actions: new Set([action]), - }); // Enforce authorization based on all *requested* object types and the current space const typesAndSpaces = objects.reduce( (acc, { type }) => (acc.has(type) ? acc : acc.set(type, new Set([namespaceString]))), // Always enforce authZ for the active space new Map>() ); - securityExtension!.enforceAuthorization({ - typesAndSpaces, - action, - typeMap, + + const { typeMap } = await securityExtension?.performAuthorization({ + actions: new Set([action]), + types: typesToAuthorize, + spaces: spacesToAuthorize, + enforceMap: typesAndSpaces, auditCallback: (error) => { if (!error) return; // We will audit success results below, after redaction for (const { type, id } of objects) { @@ -307,6 +304,9 @@ async function optionallyUseSecurity( // Is the user authorized to access this object in this space? let isAuthorizedForObject = true; try { + // ToDo: this is the only remaining call to enforceAuthorization outside of the security extension + // This was a bit complicated to change now, but can ultimately be removed when authz logic is + // migrated from the repo level to the extension level. securityExtension.enforceAuthorization({ typesAndSpaces: new Map([[type, new Set([namespaceString])]]), action, diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.test.ts index 2b2c2986aa674..3ef9aa48f30e4 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.test.ts @@ -18,7 +18,12 @@ import type { SavedObjectsBulkResolveObject, SavedObjectsBaseOptions, } from '@kbn/core-saved-objects-api-server'; -import { SavedObjectsErrorHelpers, SavedObjectsUtils } from '@kbn/core-saved-objects-utils-server'; +import { + setMapsAreEqual, + SavedObjectsErrorHelpers, + SavedObjectsUtils, + setsAreEqual, +} from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsSerializer, LEGACY_URL_ALIAS_TYPE, @@ -35,12 +40,8 @@ import { import { authMap, enforceError, - typeMapsAreEqual, - setsAreEqual, - setupCheckAuthorized, - setupCheckUnauthorized, - setupEnforceFailure, - setupEnforceSuccess, + setupPerformAuthFullyAuthorized, + setupPerformAuthEnforceFailure, setupRedactPassthrough, } from '../test_helpers/repository.test.common'; import { savedObjectsExtensionsMock } from '../mocks/saved_objects_extensions.mock'; @@ -464,22 +465,18 @@ describe('internalBulkResolve', () => { }); test(`propagates decorated error when unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect(internalBulkResolve(params)).rejects.toThrow(enforceError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`returns result when authorized`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const result = await internalBulkResolve(params); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const bulkIds = objects.map((obj) => obj.id); const expectedNamespaceString = SavedObjectsUtils.namespaceIdToString(namespace); @@ -498,57 +495,38 @@ describe('internalBulkResolve', () => { ]); }); - test(`calls checkAuthorization with type, actions, namespace, and object namespaces`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); + test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => { + setupPerformAuthFullyAuthorized(mockSecurityExt); await internalBulkResolve(params); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['bulk_get']); const expectedSpaces = new Set([namespace]); const expectedTypes = new Set([objects[0].type]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set(objects[0].type, new Set([namespace])); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); - }); - - test(`calls enforceAuthorization with action, type map, and auth map`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); - - await internalBulkResolve(params); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledWith( - expect.objectContaining({ - action: 'bulk_get', - }) - ); - - const expectedTypesAndSpaces = new Map([[objects[0].type, new Set([namespace])]]); - - const { typesAndSpaces: actualTypesAndSpaces, typeMap: actualTypeMap } = - mockSecurityExt.enforceAuthorization.mock.calls[0][0]; - - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); - expect(actualTypeMap).toBe(authMap); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toBeUndefined(); }); test(`calls redactNamespaces with authorization map`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); await internalBulkResolve(params); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledTimes(objects.length); objects.forEach((obj, i) => { @@ -565,8 +543,7 @@ describe('internalBulkResolve', () => { }); test(`adds audit event per object when successful`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); await internalBulkResolve(params); @@ -581,8 +558,7 @@ describe('internalBulkResolve', () => { }); test(`adds audit event per object when not successful`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect(internalBulkResolve(params)).rejects.toThrow(enforceError); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.ts index 634d49c83732f..747a0d5f5e6d6 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/internal_bulk_resolve.ts @@ -324,15 +324,11 @@ async function authorizeAuditAndRedact( return resolvedObjects; } - const authorizationResult = await securityExtension.checkAuthorization({ + const authorizationResult = await securityExtension?.performAuthorization({ + actions: new Set(['bulk_get']), types: new Set(typesAndSpaces.keys()), spaces: spacesToAuthorize, - actions: new Set(['bulk_get']), - }); - securityExtension.enforceAuthorization({ - typesAndSpaces, - action: 'bulk_get', - typeMap: authorizationResult.typeMap, + enforceMap: typesAndSpaces, auditCallback: (error) => { for (const { type, id } of auditableObjects) { securityExtension.addAuditEvent({ diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.security_extension.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.security_extension.test.ts index eea97945b4dda..dad313988e1c6 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.security_extension.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.security_extension.test.ts @@ -27,6 +27,11 @@ import { SavedObjectsRawDocSource, AuthorizationTypeEntry, } from '@kbn/core-saved-objects-server'; +import { + setMapsAreEqual, + arrayMapsAreEqual, + setsAreEqual, +} from '@kbn/core-saved-objects-utils-server'; import { kibanaMigratorMock } from '../mocks'; import { createRegistry, @@ -36,16 +41,10 @@ import { mockTimestamp, checkAuthError, getSuccess, - setupCheckUnauthorized, - setupEnforceFailure, enforceError, - setupCheckAuthorized, setupRedactPassthrough, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, - setsAreEqual, - typeMapsAreEqual, authMap, - setupEnforceSuccess, updateSuccess, deleteSuccess, removeReferencesToSuccess, @@ -55,7 +54,6 @@ import { mockTimestampFields, mockVersion, NAMESPACE_AGNOSTIC_TYPE, - setupCheckPartiallyAuthorized, bulkGetSuccess, expectBulkGetResult, bulkCreateSuccess, @@ -65,7 +63,10 @@ import { expectUpdateResult, bulkDeleteSuccess, createBulkDeleteSuccessStatus, - namespaceMapsAreEqual, + setupPerformAuthFullyAuthorized, + setupPerformAuthPartiallyAuthorized, + setupPerformAuthUnauthorized, + setupPerformAuthEnforceFailure, } from '../test_helpers/repository.test.common'; import { savedObjectsExtensionsMock } from '../mocks/saved_objects_extensions.mock'; @@ -121,53 +122,47 @@ describe('SavedObjectsRepository Security Extension', () => { // create a mock saved objects encryption extension mockSecurityExt = savedObjectsExtensionsMock.createSecurityExtension(); - mockGetCurrentTime.mockReturnValue(mockTimestamp); - mockGetSearchDsl.mockClear(); - repository = instantiateRepository(); }); afterEach(() => { - mockSecurityExt.checkAuthorization.mockClear(); + mockSecurityExt.performAuthorization.mockClear(); mockSecurityExt.redactNamespaces.mockClear(); + mockGetSearchDsl.mockClear(); }); describe('#get', () => { - test(`propagates decorated error when checkAuthorization rejects promise`, async () => { - mockSecurityExt.checkAuthorization.mockRejectedValueOnce(checkAuthError); + test(`propagates decorated error when performAuthorization rejects promise`, async () => { + mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError); await expect( getSuccess(client, repository, registry, type, id, { namespace }) ).rejects.toThrow(checkAuthError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).not.toHaveBeenCalled(); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`propagates decorated error when unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect( getSuccess(client, repository, registry, type, id, { namespace }) ).rejects.toThrow(enforceError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`returns result when authorized`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const result = await getSuccess(client, repository, registry, type, id, { namespace }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(client.get).toHaveBeenCalledTimes(1); expect(result).toEqual(expect.objectContaining({ type, id, namespaces: [namespace] })); }); - test(`calls checkAuthorization with type, actions, and namespaces`, async () => { + test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => { await getSuccess( client, repository, @@ -181,53 +176,35 @@ describe('SavedObjectsRepository Security Extension', () => { multiNamespaceObjNamespaces // all of the object's namespaces from preflight check are added to the auth check call ); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['get']); const expectedSpaces = new Set(multiNamespaceObjNamespaces); const expectedTypes = new Set([MULTI_NAMESPACE_CUSTOM_INDEX_TYPE]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, new Set([namespace])); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); - }); - - test(`calls enforceAuthorization with action, type map, and auth map`, async () => { - setupCheckAuthorized(mockSecurityExt); - - await getSuccess(client, repository, registry, type, id, { namespace }); - - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledWith( - expect.objectContaining({ - action: 'get', - }) - ); - - const expectedTypesAndSpaces = new Map([[type, new Set([namespace])]]); - - const { typesAndSpaces: actualTypesAndSpaces, typeMap: actualTypeMap } = - mockSecurityExt.enforceAuthorization.mock.calls[0][0]; - - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); - expect(actualTypeMap).toBe(authMap); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toBeUndefined(); }); test(`calls redactNamespaces with authorization map`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); await getSuccess(client, repository, registry, type, id, { namespace }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledTimes(1); expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledWith( expect.objectContaining({ @@ -238,9 +215,8 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit event when successful`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); await getSuccess(client, repository, registry, type, id, { namespace }); @@ -252,8 +228,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit event when not successful`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect( getSuccess(client, repository, registry, type, id, { namespace }) @@ -269,44 +244,40 @@ describe('SavedObjectsRepository Security Extension', () => { }); describe('#update', () => { - test(`propagates decorated error when checkAuthorization rejects promise`, async () => { - mockSecurityExt.checkAuthorization.mockRejectedValueOnce(checkAuthError); + test(`propagates decorated error when performAuthorization rejects promise`, async () => { + mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError); await expect( updateSuccess(client, repository, registry, type, id, attributes, { namespace }) ).rejects.toThrow(checkAuthError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).not.toHaveBeenCalled(); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`propagates decorated error when unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect( updateSuccess(client, repository, registry, type, id, attributes, { namespace }) ).rejects.toThrow(enforceError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`returns result when authorized`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const result = await updateSuccess(client, repository, registry, type, id, attributes, { namespace, }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(client.update).toHaveBeenCalledTimes(1); expect(result).toEqual( expect.objectContaining({ id, type, attributes, namespaces: [namespace] }) ); }); - test(`calls checkAuthorization with type, actions, and namespaces`, async () => { + test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => { await updateSuccess( client, repository, @@ -321,67 +292,35 @@ describe('SavedObjectsRepository Security Extension', () => { multiNamespaceObjNamespaces // all of the object's namespaces from preflight check are added to the auth check call ); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['update']); const expectedSpaces = new Set(multiNamespaceObjNamespaces); const expectedTypes = new Set([MULTI_NAMESPACE_CUSTOM_INDEX_TYPE]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, new Set([namespace])); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); - }); - - test(`calls enforceAuthorization with action, type map, and auth map`, async () => { - mockSecurityExt.checkAuthorization.mockResolvedValue({ - status: 'fully_authorized', - typeMap: authMap, - }); - - await updateSuccess( - client, - repository, - registry, - type, - id, - attributes, - { namespace }, - undefined, - multiNamespaceObjNamespaces - ); - - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledWith( - expect.objectContaining({ - action: 'update', - }) - ); - - const expectedTypesAndSpaces = new Map([[type, new Set([namespace])]]); - - const { typesAndSpaces: actualTypesAndSpaces, typeMap: actualTypeMap } = - mockSecurityExt.enforceAuthorization.mock.calls[0][0]; - - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); - expect(actualTypeMap).toBe(authMap); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toBeUndefined(); }); test(`calls redactNamespaces with authorization map`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); await updateSuccess(client, repository, registry, type, id, attributes, { namespace }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledTimes(1); expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledWith( expect.objectContaining({ @@ -392,9 +331,8 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit event when successful`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); await updateSuccess(client, repository, registry, type, id, attributes, { namespace }); @@ -407,8 +345,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); }); test(`adds audit event when not successful`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect( updateSuccess(client, repository, registry, type, id, { namespace }) @@ -424,37 +361,33 @@ describe('SavedObjectsRepository Security Extension', () => { }); describe('#create', () => { - test(`propagates decorated error when checkAuthorization rejects promise`, async () => { - mockSecurityExt.checkAuthorization.mockRejectedValueOnce(checkAuthError); + test(`propagates decorated error when performAuthorization rejects promise`, async () => { + mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError); await expect(repository.create(type, attributes, { namespace })).rejects.toThrow( checkAuthError ); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).not.toHaveBeenCalled(); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`propagates decorated error when unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect(repository.create(type, attributes, { namespace })).rejects.toThrow( enforceError ); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`returns result when authorized`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const result = await repository.create(type, attributes, { namespace, }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(client.create).toHaveBeenCalledTimes(1); expect(result).toEqual( expect.objectContaining({ @@ -466,84 +399,71 @@ describe('SavedObjectsRepository Security Extension', () => { ); }); - test(`calls checkAuthorization with type, actions, and namespace`, async () => { + test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => { await repository.create(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, attributes, { namespace, }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['create']); const expectedSpaces = new Set([namespace]); const expectedTypes = new Set([MULTI_NAMESPACE_CUSTOM_INDEX_TYPE]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, new Set([namespace])); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toEqual(expect.objectContaining({ allowGlobalResource: true })); }); - test(`calls checkAuthorization with type, actions, namespace, and initial namespaces`, async () => { + test(`calls performAuthorization with initial namespaces`, async () => { await repository.create(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, attributes, { namespace, initialNamespaces: multiNamespaceObjNamespaces, }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['create']); const expectedSpaces = new Set(multiNamespaceObjNamespaces); const expectedTypes = new Set([MULTI_NAMESPACE_CUSTOM_INDEX_TYPE]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set( + MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, + new Set(multiNamespaceObjNamespaces) + ); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); - }); - - test(`calls enforceAuthorization with action, type map, and auth map`, async () => { - setupCheckAuthorized(mockSecurityExt); - - await repository.create(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, attributes, { - namespace, - initialNamespaces: multiNamespaceObjNamespaces, - }); - - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledWith( - expect.objectContaining({ - action: 'create', - }) - ); - - const expectedTypesAndSpaces = new Map([ - [MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, new Set(multiNamespaceObjNamespaces)], - ]); - - const { typesAndSpaces: actualTypesAndSpaces, typeMap: actualTypeMap } = - mockSecurityExt.enforceAuthorization.mock.calls[0][0]; - - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); - expect(actualTypeMap).toBe(authMap); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toEqual(expect.objectContaining({ allowGlobalResource: true })); }); test(`calls redactNamespaces with authorization map`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); await repository.create(type, attributes, { namespace }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledTimes(1); expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledWith( @@ -559,8 +479,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit event when successful`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); await repository.create(type, attributes, { namespace }); @@ -578,8 +497,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit event when not successful`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect(repository.create(type, attributes, { namespace })).rejects.toThrow( enforceError @@ -606,92 +524,68 @@ describe('SavedObjectsRepository Security Extension', () => { mockDeleteLegacyUrlAliases.mockClear(); }); - test(`propagates decorated error when checkAuthorization rejects promise`, async () => { - mockSecurityExt.checkAuthorization.mockRejectedValueOnce(checkAuthError); + test(`propagates decorated error when performAuthorization rejects promise`, async () => { + mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError); await expect( deleteSuccess(client, repository, registry, type, id, { namespace }) ).rejects.toThrow(checkAuthError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).not.toHaveBeenCalled(); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`propagates decorated error when unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect( deleteSuccess(client, repository, registry, type, id, { namespace }) ).rejects.toThrow(enforceError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`returns empty object result when authorized`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const result = await deleteSuccess(client, repository, registry, type, id, { namespace, }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(client.delete).toHaveBeenCalledTimes(1); expect(result).toEqual({}); }); - test(`calls checkAuthorization with type, actions, and namespaces`, async () => { + test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => { await deleteSuccess(client, repository, registry, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, id, { namespace, force: true, }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['delete']); const expectedSpaces = new Set([namespace]); const expectedTypes = new Set([MULTI_NAMESPACE_CUSTOM_INDEX_TYPE]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set(MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, new Set([namespace])); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); - }); - - test(`calls enforceAuthorization with action, type map, and auth map`, async () => { - setupCheckAuthorized(mockSecurityExt); - - await deleteSuccess(client, repository, registry, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, id, { - namespace, - }); - - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledWith( - expect.objectContaining({ - action: 'delete', - }) - ); - const expectedTypesAndSpaces = new Map([ - [MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, new Set([namespace])], - ]); - - const { typesAndSpaces: actualTypesAndSpaces, typeMap: actualTypeMap } = - mockSecurityExt.enforceAuthorization.mock.calls[0][0]; - - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); - expect(actualTypeMap).toBe(authMap); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toBeUndefined(); }); test(`adds audit event when successful`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); await deleteSuccess(client, repository, registry, type, id, { namespace }); @@ -705,8 +599,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit event when not successful`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect( deleteSuccess(client, repository, registry, type, id, { namespace }) @@ -722,84 +615,63 @@ describe('SavedObjectsRepository Security Extension', () => { }); describe('#removeReferencesTo', () => { - test(`propagates decorated error when checkAuthorization rejects promise`, async () => { - mockSecurityExt.checkAuthorization.mockRejectedValueOnce(checkAuthError); + test(`propagates decorated error when performAuthorization rejects promise`, async () => { + mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError); await expect( removeReferencesToSuccess(client, repository, type, id, { namespace }) ).rejects.toThrow(checkAuthError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).not.toHaveBeenCalled(); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`propagates decorated error when unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect( removeReferencesToSuccess(client, repository, type, id, { namespace }) ).rejects.toThrow(enforceError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`returns result when authorized`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const result = await removeReferencesToSuccess(client, repository, type, id, { namespace }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(client.updateByQuery).toHaveBeenCalledTimes(1); expect(result).toEqual(expect.objectContaining({ updated: REMOVE_REFS_COUNT })); }); - test(`calls checkAuthorization with type, actions, and namespaces`, async () => { + test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => { await removeReferencesToSuccess(client, repository, type, id, { namespace }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['delete']); const expectedSpaces = new Set([namespace]); const expectedTypes = new Set([type]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set(type, new Set([namespace])); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); - }); - - test(`calls enforceAuthorization with type/namespace map`, async () => { - setupCheckAuthorized(mockSecurityExt); - - await removeReferencesToSuccess(client, repository, type, id, { namespace }); - - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledWith( - expect.objectContaining({ - action: 'delete', - }) - ); - - const expectedTypesAndSpaces = new Map([[type, new Set([namespace])]]); - - const { typesAndSpaces: actualTypesAndSpaces, typeMap: actualTypeMap } = - mockSecurityExt.enforceAuthorization.mock.calls[0][0]; - - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); - expect(actualTypeMap).toBe(authMap); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toBeUndefined(); }); test(`adds audit event when successful`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); await removeReferencesToSuccess(client, repository, type, id, { namespace }); @@ -813,8 +685,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit event when not successful`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect( removeReferencesToSuccess(client, repository, type, id, { namespace }) @@ -833,37 +704,33 @@ describe('SavedObjectsRepository Security Extension', () => { const obj1 = { type, id: 'one' }; const obj2 = { type, id: 'two' }; - test(`propagates decorated error when checkAuthorization rejects promise`, async () => { - mockSecurityExt.checkAuthorization.mockRejectedValueOnce(checkAuthError); + test(`propagates decorated error when performAuthorization rejects promise`, async () => { + mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError); await expect( checkConflictsSuccess(client, repository, registry, [obj1, obj2], { namespace }) ).rejects.toThrow(checkAuthError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).not.toHaveBeenCalled(); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`propagates decorated error when unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect( checkConflictsSuccess(client, repository, registry, [obj1, obj2], { namespace }) ).rejects.toThrow(enforceError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`returns result when authorized`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const result = await checkConflictsSuccess(client, repository, registry, [obj1, obj2], { namespace, }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(client.mget).toHaveBeenCalledTimes(1); // Default mock mget makes each object found expect(result).toEqual( @@ -892,69 +759,53 @@ describe('SavedObjectsRepository Security Extension', () => { ); }); - test(`calls checkAuthorization with type, actions, and namespaces`, async () => { + test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => { await checkConflictsSuccess(client, repository, registry, [obj1, obj2], { namespace }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['bulk_create']); const expectedSpaces = new Set([namespace]); - const expectedTypes = new Set([type]); + const expectedTypes = new Set([obj1.type, obj2.type]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set(obj1.type, new Set([namespace])); + expectedEnforceMap.set(obj2.type, new Set([namespace])); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); - }); - - test(`calls enforceAuthorization with type/namespace map`, async () => { - setupCheckAuthorized(mockSecurityExt); - - await checkConflictsSuccess(client, repository, registry, [obj1, obj2], { namespace }); - - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledWith( - expect.objectContaining({ - action: 'bulk_create', - }) - ); - const expectedTypesAndSpaces = new Map([[type, new Set([namespace])]]); - - const { typesAndSpaces: actualTypesAndSpaces, typeMap: actualTypeMap } = - mockSecurityExt.enforceAuthorization.mock.calls[0][0]; - - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); - expect(actualTypeMap).toBe(authMap); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toBeUndefined(); }); }); describe('#openPointInTimeForType', () => { - test(`propagates decorated error when checkAuthorization rejects promise`, async () => { - mockSecurityExt.checkAuthorization.mockRejectedValueOnce(checkAuthError); + test(`propagates decorated error when performAuthorization rejects promise`, async () => { + mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError); await expect(repository.openPointInTimeForType(type)).rejects.toThrow(checkAuthError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).not.toHaveBeenCalled(); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`returns result when authorized`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); client.openPointInTime.mockResponseOnce({ id }); const result = await repository.openPointInTimeForType(type); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).not.toHaveBeenCalled(); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(client.openPointInTime).toHaveBeenCalledTimes(1); expect(result).toEqual(expect.objectContaining({ id })); }); test(`adds audit event when successful`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); client.openPointInTime.mockResponseOnce({ id }); await repository.openPointInTimeForType(type); @@ -967,12 +818,12 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`throws an error when unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); + setupPerformAuthUnauthorized(mockSecurityExt); await expect(repository.openPointInTimeForType(type)).rejects.toThrowError(); }); test(`adds audit event when unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); + setupPerformAuthUnauthorized(mockSecurityExt); await expect(repository.openPointInTimeForType(type)).rejects.toThrowError(); @@ -983,12 +834,12 @@ describe('SavedObjectsRepository Security Extension', () => { }); }); - test(`calls checkAuthorization with type, actions, and namespaces`, async () => { - setupCheckAuthorized(mockSecurityExt); + test(`calls performAuthorization with correct actions, types, and spaces`, async () => { + setupPerformAuthFullyAuthorized(mockSecurityExt); client.openPointInTime.mockResponseOnce({ id }); await repository.openPointInTimeForType(type, { namespaces: [namespace] }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['open_point_in_time']); const expectedSpaces = new Set([namespace]); const expectedTypes = new Set([type]); @@ -997,11 +848,15 @@ describe('SavedObjectsRepository Security Extension', () => { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); + expect(actualEnforceMap).toBeUndefined(); + expect(actualOptions).toBeUndefined(); }); }); @@ -1026,19 +881,18 @@ describe('SavedObjectsRepository Security Extension', () => { }); describe('#find', () => { - test(`propagates decorated error when checkAuthorization rejects promise`, async () => { - mockSecurityExt.checkAuthorization.mockRejectedValueOnce(checkAuthError); + test(`propagates decorated error when Authorization rejects promise`, async () => { + mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError); await expect(findSuccess(client, repository, { type })).rejects.toThrow(checkAuthError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).not.toHaveBeenCalled(); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`returns empty result when unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); + setupPerformAuthUnauthorized(mockSecurityExt); const result = await repository.find({ type }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(result).toEqual( expect.objectContaining({ saved_objects: [], @@ -1052,7 +906,7 @@ describe('SavedObjectsRepository Security Extension', () => { const authRecord: Record = { find: { authorizedSpaces: [namespace] }, }; - mockSecurityExt.checkAuthorization.mockResolvedValue({ + mockSecurityExt.performAuthorization.mockResolvedValue({ status: 'partially_authorized', typeMap: Object.freeze(new Map([[type, authRecord]])), }); @@ -1067,11 +921,11 @@ describe('SavedObjectsRepository Security Extension', () => { expect(actualMap).toBeDefined(); const expectedMap = new Map(); expectedMap.set(type, [namespace]); - expect(namespaceMapsAreEqual(actualMap, expectedMap)).toBeTruthy(); + expect(arrayMapsAreEqual(actualMap, expectedMap)).toBeTruthy(); }); test(`returns result of es find when fully authorized`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const { result, generatedResults } = await findSuccess( @@ -1101,7 +955,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`uses the authorization map when partially authorized`, async () => { - setupCheckPartiallyAuthorized(mockSecurityExt); + setupPerformAuthPartiallyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); await findSuccess( @@ -1111,18 +965,21 @@ describe('SavedObjectsRepository Security Extension', () => { namespace ); - // make sure the authorized map gets passed to the es client call - expect(mockGetSearchDsl).toHaveBeenCalledWith( - expect.objectContaining({}), - expect.objectContaining({}), - expect.objectContaining({ - typeToNamespacesMap: authMap, - }) - ); + expect(mockGetSearchDsl.mock.calls[0].length).toBe(3); // Find success verifies this is called once, this should always pass + const { + typeToNamespacesMap: actualMap, + }: { typeToNamespacesMap: Map } = + mockGetSearchDsl.mock.calls[0][2]; + + expect(actualMap).not.toBeUndefined(); + const expectedMap = new Map(); + expectedMap.set('foo', ['bar']); // this is what is hard-coded in authMap + + expect(arrayMapsAreEqual(actualMap, expectedMap)).toBeTruthy(); }); test(`returns result of es find when partially authorized`, async () => { - setupCheckPartiallyAuthorized(mockSecurityExt); + setupPerformAuthPartiallyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const { result, generatedResults } = await findSuccess( @@ -1151,12 +1008,12 @@ describe('SavedObjectsRepository Security Extension', () => { }); }); - test(`calls checkAuthorization with type, actions, and namespaces`, async () => { - setupCheckPartiallyAuthorized(mockSecurityExt); + test(`calls performAuthorization with correct actions, types, and spaces`, async () => { + setupPerformAuthPartiallyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); - await findSuccess(client, repository, { type, namespaces: [namespace] }); + await findSuccess(client, repository, { type, namespaces: [namespace] }, 'ns-2'); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(2); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(2); const expectedActions = new Set(['find']); const expectedSpaces = new Set([namespace]); const expectedTypes = new Set([type]); @@ -1165,15 +1022,33 @@ describe('SavedObjectsRepository Security Extension', () => { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); + expect(actualEnforceMap).toBeUndefined(); + expect(actualOptions).toBeUndefined(); + + const { + actions: actualActions2, + spaces: actualSpaces2, + types: actualTypes2, + enforceMap: actualEnforceMap2, + options: actualOptions2, + } = mockSecurityExt.performAuthorization.mock.calls[1][0]; + + expect(setsAreEqual(actualActions2, expectedActions)).toBeTruthy(); + expect(setsAreEqual(actualSpaces2, new Set([...expectedSpaces, 'ns-2']))).toBeTruthy(); + expect(setsAreEqual(actualTypes2, expectedTypes)).toBeTruthy(); + expect(actualEnforceMap2).toBeUndefined(); + expect(actualOptions2).toBeUndefined(); }); test(`calls redactNamespaces with authorization map`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const { generatedResults } = await findSuccess(client, repository, { @@ -1192,7 +1067,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit per object event when successful`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const { generatedResults } = await findSuccess(client, repository, { @@ -1218,7 +1093,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit event when not successful`, async () => { - setupCheckUnauthorized(mockSecurityExt); + setupPerformAuthUnauthorized(mockSecurityExt); await repository.find({ type }); @@ -1259,44 +1134,26 @@ describe('SavedObjectsRepository Security Extension', () => { namespaces: [namespace], }; - test(`propagates decorated error when checkAuthorization rejects promise`, async () => { - mockSecurityExt.checkAuthorization.mockRejectedValueOnce(checkAuthError); + test(`propagates decorated error when performAuthorization rejects promise`, async () => { + mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError); await expect( bulkGetSuccess(client, repository, registry, [obj1, obj2], { namespace }) ).rejects.toThrow(checkAuthError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).not.toHaveBeenCalled(); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`propagates decorated error when unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect( bulkGetSuccess(client, repository, registry, [obj1, obj2], { namespace }) ).rejects.toThrow(enforceError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledWith( - expect.objectContaining({ - action: 'bulk_get', - }) - ); - - const expectedTypesAndSpaces = new Map([ - [obj1.type, new Set([namespace])], - [obj2.type, new Set([namespace])], - ]); - - const { typesAndSpaces: actualTypesAndSpaces } = - mockSecurityExt.enforceAuthorization.mock.calls[0][0]; - - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`returns result when authorized`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const { result, mockResponse } = await bulkGetSuccess( @@ -1307,8 +1164,7 @@ describe('SavedObjectsRepository Security Extension', () => { { namespace } ); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(client.mget).toHaveBeenCalledTimes(1); expect(result).toEqual({ saved_objects: [ @@ -1324,36 +1180,40 @@ describe('SavedObjectsRepository Security Extension', () => { }); }); - test(`calls checkAuthorization with type, actions, namespace, and object namespaces`, async () => { + test(`calls performAuthorization with correct parameters in default space`, async () => { const objA = { ...obj1, type: MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, // replace the type to a mult-namespace type for this test to be thorough namespaces: multiNamespaceObjNamespaces, // include multiple spaces }; const objB = { ...obj2, namespaces: ['ns-3'] }; // use a different namespace than the options namespace; - const optionsNamespace = 'ns-4'; - await bulkGetSuccess(client, repository, registry, [objA, objB], { - namespace: optionsNamespace, - }); + await bulkGetSuccess(client, repository, registry, [objA, objB]); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['bulk_get']); - const expectedSpaces = new Set([optionsNamespace, ...objA.namespaces, ...objB.namespaces]); + const expectedSpaces = new Set(['default', ...objA.namespaces, ...objB.namespaces]); const expectedTypes = new Set([objA.type, objB.type]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set(objA.type, new Set(['default', ...objA.namespaces])); + expectedEnforceMap.set(objB.type, new Set(['default', ...objB.namespaces])); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toBeUndefined(); }); - test(`calls enforceAuthorization with action, type map, and auth map`, async () => { + test(`calls performAuthorization with correct parameters in non-default space`, async () => { const objA = { ...obj1, type: MULTI_NAMESPACE_CUSTOM_INDEX_TYPE, // replace the type to a mult-namespace type for this test to be thorough @@ -1362,43 +1222,42 @@ describe('SavedObjectsRepository Security Extension', () => { const objB = { ...obj2, namespaces: ['ns-3'] }; // use a different namespace than the options namespace; const optionsNamespace = 'ns-4'; - setupCheckAuthorized(mockSecurityExt); - await bulkGetSuccess(client, repository, registry, [objA, objB], { namespace: optionsNamespace, }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledWith( - expect.objectContaining({ - action: 'bulk_get', - }) - ); - - const expectedTypesAndSpaces = new Map([ - [objA.type, new Set([optionsNamespace, ...objA.namespaces])], - [objB.type, new Set([optionsNamespace, ...objB.namespaces])], - ]); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); + const expectedActions = new Set(['bulk_get']); + const expectedSpaces = new Set([optionsNamespace, ...objA.namespaces, ...objB.namespaces]); + const expectedTypes = new Set([objA.type, objB.type]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set(objA.type, new Set([optionsNamespace, ...objA.namespaces])); + expectedEnforceMap.set(objB.type, new Set([optionsNamespace, ...objB.namespaces])); - const { typesAndSpaces: actualTypesAndSpaces, typeMap: actualTypeMap } = - mockSecurityExt.enforceAuthorization.mock.calls[0][0]; + const { + actions: actualActions, + spaces: actualSpaces, + types: actualTypes, + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); - expect(actualTypeMap).toBe(authMap); // ToDo? reference comparison ok, object is frozen + expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); + expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); + expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toBeUndefined(); }); test(`calls redactNamespaces with authorization map`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const objects = [obj1, obj2]; await bulkGetSuccess(client, repository, registry, objects, { namespace }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledTimes(2); objects.forEach((obj, i) => { @@ -1415,9 +1274,8 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit event per object when successful`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); const objects = [obj1, obj2]; await bulkGetSuccess(client, repository, registry, objects, { namespace }); @@ -1432,8 +1290,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit event per object when not successful`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); const objects = [obj1, obj2]; await expect( @@ -1473,66 +1330,69 @@ describe('SavedObjectsRepository Security Extension', () => { references: [{ name: 'ref_0', type: 'test', id: '2' }], }; - test(`propagates decorated error when checkAuthorization rejects promise`, async () => { - mockSecurityExt.checkAuthorization.mockRejectedValueOnce(checkAuthError); + test(`propagates decorated error when performAuthorization rejects promise`, async () => { + mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError); await expect(bulkCreateSuccess(client, repository, [obj1, obj2])).rejects.toThrow( checkAuthError ); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).not.toHaveBeenCalled(); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`propagates decorated error when unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect( bulkCreateSuccess(client, repository, [obj1, obj2], { namespace }) ).rejects.toThrow(enforceError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`returns result when authorized`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const objects = [obj1, obj2]; const result = await bulkCreateSuccess(client, repository, objects); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(client.bulk).toHaveBeenCalledTimes(1); expect(result).toEqual({ saved_objects: objects.map((obj) => expectCreateResult(obj)), }); }); - test(`calls checkAuthorization with type, actions, and namespace`, async () => { - setupCheckAuthorized(mockSecurityExt); + test(`calls PerformAuthorization with correct actions, types, spaces, and enforce map`, async () => { + setupPerformAuthFullyAuthorized(mockSecurityExt); await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace, }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['bulk_create']); const expectedSpaces = new Set([namespace]); const expectedTypes = new Set([obj1.type, obj2.type]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set(obj1.type, new Set([namespace])); + expectedEnforceMap.set(obj2.type, new Set([namespace])); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toEqual(expect.objectContaining({ allowGlobalResource: true })); }); - test(`calls checkAuthorization with type, actions, namespace, and initial namespaces`, async () => { + test(`calls performAuthorization with initial spaces for one type`, async () => { const objA = { ...obj1, type: MULTI_NAMESPACE_TYPE, @@ -1545,14 +1405,13 @@ describe('SavedObjectsRepository Security Extension', () => { }; const optionsNamespace = 'ns-5'; - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); await bulkCreateSuccess(client, repository, [objA, objB], { namespace: optionsNamespace, }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['bulk_create']); const expectedSpaces = new Set([ optionsNamespace, @@ -1560,19 +1419,28 @@ describe('SavedObjectsRepository Security Extension', () => { ...objB.initialNamespaces, ]); const expectedTypes = new Set([objA.type, objB.type]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set( + objA.type, + new Set([optionsNamespace, ...objA.initialNamespaces, ...objB.initialNamespaces]) + ); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toEqual(expect.objectContaining({ allowGlobalResource: true })); }); - test(`calls enforceAuthorization with action, type map, and auth map`, async () => { + test(`calls performAuthorization with initial spaces for multiple types`, async () => { const objA = { ...obj1, type: MULTI_NAMESPACE_TYPE, @@ -1585,41 +1453,48 @@ describe('SavedObjectsRepository Security Extension', () => { }; const optionsNamespace = 'ns-5'; - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); await bulkCreateSuccess(client, repository, [objA, objB], { namespace: optionsNamespace, }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledWith( - expect.objectContaining({ - action: 'bulk_create', - }) - ); - const expectedTypesAndSpaces = new Map([ - [objA.type, new Set([optionsNamespace, ...objA.initialNamespaces])], - [objB.type, new Set([optionsNamespace, ...objB.initialNamespaces])], + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); + + const expectedActions = new Set(['bulk_create']); + const expectedSpaces = new Set([ + optionsNamespace, + ...objA.initialNamespaces, + ...objB.initialNamespaces, ]); + const expectedTypes = new Set([objA.type, objB.type]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set(objA.type, new Set([optionsNamespace, ...objA.initialNamespaces])); + expectedEnforceMap.set(objB.type, new Set([optionsNamespace, ...objB.initialNamespaces])); - const { typesAndSpaces: actualTypesAndSpaces, typeMap: actualTypeMap } = - mockSecurityExt.enforceAuthorization.mock.calls[0][0]; + const { + actions: actualActions, + spaces: actualSpaces, + types: actualTypes, + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); - expect(actualTypeMap).toBe(authMap); + expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); + expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); + expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toEqual(expect.objectContaining({ allowGlobalResource: true })); }); test(`calls redactNamespaces with authorization map`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const objects = [obj1, obj2]; await bulkCreateSuccess(client, repository, [obj1, obj2], { namespace }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledTimes(2); objects.forEach((obj, i) => { @@ -1636,8 +1511,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit event per object when successful`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); const objects = [obj1, obj2]; await bulkCreateSuccess(client, repository, objects, { namespace }); @@ -1653,8 +1527,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit event per object when not successful`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); const objects = [obj1, obj2]; await expect(bulkCreateSuccess(client, repository, objects, { namespace })).rejects.toThrow( @@ -1684,66 +1557,69 @@ describe('SavedObjectsRepository Security Extension', () => { attributes: { title: 'Test Two' }, }; - test(`propagates decorated error when checkAuthorization rejects promise`, async () => { - mockSecurityExt.checkAuthorization.mockRejectedValueOnce(checkAuthError); + test(`propagates decorated error when performAuthorization rejects promise`, async () => { + mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError); await expect(bulkUpdateSuccess(client, repository, registry, [obj1, obj2])).rejects.toThrow( checkAuthError ); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).not.toHaveBeenCalled(); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`propagates decorated error when unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect(bulkUpdateSuccess(client, repository, registry, [obj1, obj2])).rejects.toThrow( enforceError ); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`returns result when authorized`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const objects = [obj1, obj2]; const result = await bulkUpdateSuccess(client, repository, registry, objects); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(client.bulk).toHaveBeenCalledTimes(1); expect(result).toEqual({ saved_objects: objects.map((obj) => expectUpdateResult(obj)), }); }); - test(`calls checkAuthorization with type, actions, and namespace`, async () => { - setupCheckAuthorized(mockSecurityExt); + test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => { + setupPerformAuthFullyAuthorized(mockSecurityExt); await bulkUpdateSuccess(client, repository, registry, [obj1, obj2], { namespace, }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['bulk_update']); const expectedSpaces = new Set([namespace]); const expectedTypes = new Set([obj1.type, obj2.type]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set(obj1.type, new Set([namespace])); + expectedEnforceMap.set(obj2.type, new Set([namespace])); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toBeUndefined(); }); - test(`calls checkAuthorization with type, actions, namespace, and object namespaces`, async () => { + test(`calls performAuthorization with object spaces`, async () => { const objA = { ...obj1, namespace: 'ns-1', // object namespace @@ -1753,73 +1629,43 @@ describe('SavedObjectsRepository Security Extension', () => { namespace: 'ns-2', // object namespace }; - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); await bulkUpdateSuccess(client, repository, registry, [objA, objB], { namespace, }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['bulk_update']); const expectedSpaces = new Set([namespace, objA.namespace, objB.namespace]); const expectedTypes = new Set([objA.type, objB.type]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set(objA.type, new Set([namespace, objA.namespace])); + expectedEnforceMap.set(objB.type, new Set([namespace, objB.namespace])); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); - }); - - test(`calls enforceAuthorization with action, type map, and auth map`, async () => { - const objA = { - ...obj1, - namespace: 'ns-1', // object namespace - }; - const objB = { - ...obj2, - namespace: 'ns-2', // object namespace - }; - - setupCheckAuthorized(mockSecurityExt); - - await bulkUpdateSuccess(client, repository, registry, [objA, objB], { - namespace, - }); - - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledWith( - expect.objectContaining({ - action: 'bulk_update', - }) - ); - - const expectedTypesAndSpaces = new Map([ - [objA.type, new Set([namespace, objA.namespace])], - [objB.type, new Set([namespace, objB.namespace])], - ]); - - const { typesAndSpaces: actualTypesAndSpaces, typeMap: actualTypeMap } = - mockSecurityExt.enforceAuthorization.mock.calls[0][0]; - - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); - expect(actualTypeMap).toBe(authMap); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toBeUndefined(); }); test(`calls redactNamespaces with authorization map`, async () => { - setupCheckAuthorized(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const objects = [obj1, obj2]; await bulkUpdateSuccess(client, repository, registry, objects, { namespace }); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(mockSecurityExt.redactNamespaces).toHaveBeenCalledTimes(2); objects.forEach((obj, i) => { @@ -1836,8 +1682,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit event per object when successful`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); const objects = [obj1, obj2]; await bulkUpdateSuccess(client, repository, registry, objects, { namespace }); @@ -1853,8 +1698,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit event per object when not successful`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); const objects = [obj1, obj2]; await expect( @@ -1906,30 +1750,26 @@ describe('SavedObjectsRepository Security Extension', () => { ], }; - test(`propagates decorated error when checkAuthorization rejects promise`, async () => { - mockSecurityExt.checkAuthorization.mockRejectedValueOnce(checkAuthError); + test(`propagates decorated error when performAuthorization rejects promise`, async () => { + mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError); await expect( bulkDeleteSuccess(client, repository, registry, testObjs, options) ).rejects.toThrow(checkAuthError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).not.toHaveBeenCalled(); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`propagates decorated error when unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect( bulkDeleteSuccess(client, repository, registry, testObjs, options) ).rejects.toThrow(enforceError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`returns result when authorized`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const result = await bulkDeleteSuccess( @@ -1941,63 +1781,41 @@ describe('SavedObjectsRepository Security Extension', () => { internalOptions ); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(client.bulk).toHaveBeenCalledTimes(1); expect(result).toEqual({ statuses: testObjs.map((obj) => createBulkDeleteSuccessStatus(obj)), }); }); - test(`calls checkAuthorization with type, actions, and namespaces`, async () => { - setupCheckAuthorized(mockSecurityExt); + test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => { + setupPerformAuthFullyAuthorized(mockSecurityExt); await bulkDeleteSuccess(client, repository, registry, testObjs, options, internalOptions); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['bulk_delete']); - const expectedSpaces = new Set(internalOptions.mockMGetResponseObjects[1].initialNamespaces); + const exptectedSpaces = new Set(internalOptions.mockMGetResponseObjects[1].initialNamespaces); const expectedTypes = new Set([obj1.type, obj2.type]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set(obj1.type, new Set([namespace])); + expectedEnforceMap.set(obj2.type, new Set([namespace])); const { actions: actualActions, - spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + spaces: actualSpaces, + enforceMap: actualEnforceMap, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); - expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); - }); - - test(`calls enforceAuthorization with action, type map, and auth map`, async () => { - setupCheckAuthorized(mockSecurityExt); - - await bulkDeleteSuccess(client, repository, registry, testObjs, options, internalOptions); - - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledWith( - expect.objectContaining({ - action: 'bulk_delete', - }) - ); - - const expectedTypesAndSpaces = new Map([ - [obj1.type, new Set([namespace])], - [obj2.type, new Set([namespace])], // only need authz in current space - ]); - - const { typesAndSpaces: actualTypesAndSpaces, typeMap: actualTypeMap } = - mockSecurityExt.enforceAuthorization.mock.calls[0][0]; - - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); - expect(actualTypeMap).toBe(authMap); + expect(setsAreEqual(actualSpaces, exptectedSpaces)).toBeTruthy(); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); }); test(`adds audit event per object when successful`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); const objects = [obj1, obj2]; await bulkDeleteSuccess(client, repository, registry, objects, options); @@ -2013,8 +1831,7 @@ describe('SavedObjectsRepository Security Extension', () => { }); test(`adds audit event per object when not successful`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); const objects = [obj1, obj2]; await expect( diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.spaces_extension.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.spaces_extension.test.ts index c5c90b048dc31..81498a2b9df2f 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.spaces_extension.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.spaces_extension.test.ts @@ -53,7 +53,7 @@ import { bulkCreateSuccess, bulkUpdateSuccess, findSuccess, - setupCheckUnauthorized, + setupPerformAuthUnauthorized, generateIndexPatternSearchResults, bulkDeleteSuccess, } from '../test_helpers/repository.test.common'; @@ -912,7 +912,7 @@ describe('SavedObjectsRepository Spaces Extension', () => { describe(`#find`, () => { test(`returns empty result if user is unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); + setupPerformAuthUnauthorized(mockSecurityExt); const type = 'index-pattern'; const spaceOverride = 'ns-4'; const generatedResults = generateIndexPatternSearchResults(spaceOverride); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts index 7db63fc565a00..8260a7cdddec9 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts @@ -355,31 +355,25 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { const existingNamespaces = preflightResult?.existingDocument?._source?.namespaces || []; const spacesToAuthorize = new Set(existingNamespaces); spacesToAuthorize.delete(ALL_NAMESPACES_STRING); // Don't accidentally check for global privileges when the object exists in '*' - const authorizationResult = await this._securityExtension?.checkAuthorization({ - types: new Set([type]), - spaces: new Set([...spacesToEnforce, ...spacesToAuthorize]), // existing namespaces are included so we can later redact if necessary - actions: new Set(['create']), + const authorizationResult = await this._securityExtension?.performAuthorization({ // If a user tries to create an object with `initialNamespaces: ['*']`, they need to have 'create' privileges for the Global Resource // (e.g., All privileges for All Spaces). // Inversely, if a user tries to overwrite an object that already exists in '*', they don't need to 'create' privileges for the Global // Resource, so in that case we have to filter out that string from spacesToAuthorize (because `allowGlobalResource: true` is used // below.) + actions: new Set(['create']), + types: new Set([type]), + spaces: new Set([...spacesToEnforce, ...spacesToAuthorize]), // existing namespaces are included so we can later redact if necessary + enforceMap: new Map([[type, spacesToEnforce]]), + auditCallback: (error) => + this._securityExtension!.addAuditEvent({ + action: AuditAction.CREATE, + savedObject: { type, id }, + error, + ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the create operation has not occurred yet + }), options: { allowGlobalResource: true }, }); - if (authorizationResult) { - this._securityExtension!.enforceAuthorization({ - typesAndSpaces: new Map([[type, spacesToEnforce]]), - action: 'create', - typeMap: authorizationResult.typeMap, - auditCallback: (error) => - this._securityExtension!.addAuditEvent({ - action: AuditAction.CREATE, - savedObject: { type, id }, - error, - ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the create operation has not occurred yet - }), - }); - } if (preflightResult?.error) { // This intentionally occurs _after_ the authZ enforcement (which may throw a 403 error earlier) @@ -548,34 +542,28 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { } } - const authorizationResult = await this._securityExtension?.checkAuthorization({ - types: new Set(typesAndSpaces.keys()), - spaces: spacesToAuthorize, - actions: new Set(['bulk_create']), + const authorizationResult = await this._securityExtension?.performAuthorization({ // If a user tries to create an object with `initialNamespaces: ['*']`, they need to have 'bulk_create' privileges for the Global // Resource (e.g., All privileges for All Spaces). // Inversely, if a user tries to overwrite an object that already exists in '*', they don't need to have 'bulk_create' privileges for the Global // Resource, so in that case we have to filter out that string from spacesToAuthorize (because `allowGlobalResource: true` is used // below.) + actions: new Set(['bulk_create']), + types: new Set(typesAndSpaces.keys()), + spaces: spacesToAuthorize, + enforceMap: typesAndSpaces, + auditCallback: (error) => { + for (const { value } of validObjects) { + this._securityExtension!.addAuditEvent({ + action: AuditAction.CREATE, + savedObject: { type: value.object.type, id: value.object.id }, + error, + ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the create operation has not occurred yet + }); + } + }, options: { allowGlobalResource: true }, }); - if (authorizationResult) { - this._securityExtension!.enforceAuthorization({ - typesAndSpaces, - action: 'bulk_create', - typeMap: authorizationResult.typeMap, - auditCallback: (error) => { - for (const { value } of validObjects) { - this._securityExtension!.addAuditEvent({ - action: AuditAction.CREATE, - savedObject: { type: value.object.type, id: value.object.id }, - error, - ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the create operation has not occurred yet - }); - } - }, - }); - } let bulkRequestIndexCounter = 0; const bulkCreateParams: object[] = []; @@ -777,21 +765,16 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { for (const { value } of validObjects) { typesAndSpaces.set(value.type, new Set([namespaceString])); // Always enforce authZ for the active space } - const authorizationResult = await this._securityExtension?.checkAuthorization({ + + await this._securityExtension?.performAuthorization({ + actions: new Set(['bulk_create']), types: new Set(typesAndSpaces.keys()), spaces: new Set([namespaceString]), // Always check authZ for the active space - actions: new Set(['bulk_create']), + enforceMap: typesAndSpaces, + // auditCallback is intentionally omitted, this function in the previous Security SOC wrapper implementation + // did not have audit logging. This is primarily because it is only used by Kibana and is not exposed in a + // public HTTP API }); - if (authorizationResult) { - this._securityExtension!.enforceAuthorization({ - typesAndSpaces, - action: 'bulk_create', - typeMap: authorizationResult.typeMap, - // auditCallback is intentionally omitted, this function in the previous Security SOC wrapper implementation - // did not have audit logging. This is primarily because it is only used by Kibana and is not exposed in a - // public HTTP API - }); - } const bulkGetDocs = validObjects.map(({ value: { type, id } }) => ({ _id: this._serializer.generateRawId(namespace, type, id), @@ -860,26 +843,21 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { const namespaceString = SavedObjectsUtils.namespaceIdToString(namespace); const typesAndSpaces = new Map>([[type, new Set([namespaceString])]]); // Always enforce authZ for the active space - const authorizationResult = await this._securityExtension?.checkAuthorization({ + + await this._securityExtension?.performAuthorization({ + actions: new Set(['delete']), types: new Set([type]), spaces: new Set([namespaceString]), // Always check authZ for the active space - actions: new Set(['delete']), + enforceMap: typesAndSpaces, + auditCallback: (error) => { + this._securityExtension!.addAuditEvent({ + action: AuditAction.DELETE, + savedObject: { type, id }, + error, + ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the delete operation has not occurred yet + }); + }, }); - if (authorizationResult) { - this._securityExtension!.enforceAuthorization({ - typesAndSpaces, - action: 'delete', - typeMap: authorizationResult.typeMap, - auditCallback: (error) => { - this._securityExtension!.addAuditEvent({ - action: AuditAction.DELETE, - savedObject: { type, id }, - error, - ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the delete operation has not occurred yet - }); - }, - }); - } const rawId = this._serializer.generateRawId(namespace, type, id); let preflightResult: PreflightCheckNamespacesResult | undefined; @@ -1165,28 +1143,22 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { } } - const authorizationResult = await this._securityExtension?.checkAuthorization({ + await this._securityExtension?.performAuthorization({ + actions: new Set(['bulk_delete']), types: new Set(typesAndSpaces.keys()), spaces: spacesToAuthorize, - actions: new Set(['bulk_delete']), + enforceMap: typesAndSpaces, + auditCallback: (error) => { + for (const { value } of expectedBulkDeleteMultiNamespaceDocsResults) { + this._securityExtension!.addAuditEvent({ + action: AuditAction.DELETE, + savedObject: { type: value.type, id: value.id }, + error, + ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the delete operation has not occurred yet + }); + } + }, }); - if (authorizationResult) { - this._securityExtension!.enforceAuthorization({ - typesAndSpaces, - action: 'bulk_delete', - typeMap: authorizationResult.typeMap, - auditCallback: (error) => { - for (const { value } of expectedBulkDeleteMultiNamespaceDocsResults) { - this._securityExtension!.addAuditEvent({ - action: AuditAction.DELETE, - savedObject: { type: value.type, id: value.id }, - error, - ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the delete operation has not occurred yet - }); - } - }, - }); - } // Filter valid objects const validObjects = expectedBulkDeleteMultiNamespaceDocsResults.filter(isRight); @@ -1471,10 +1443,10 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { let typeToNamespacesMap: Map | undefined; let preAuthorizationResult: CheckAuthorizationResult<'find'> | undefined; if (!disableExtensions && this._securityExtension) { - preAuthorizationResult = await this._securityExtension.checkAuthorization({ + preAuthorizationResult = await this._securityExtension.performAuthorization({ + actions: new Set(['find']), types: new Set(types), spaces: spacesToPreauthorize, - actions: new Set(['find']), }); if (preAuthorizationResult.status === 'unauthorized') { // If the user is unauthorized to find *anything* they requested, return an empty response @@ -1584,10 +1556,10 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { spacesToAuthorize.size > spacesToPreauthorize.size ? // If there are any namespaces in the object results that were not already checked during pre-authorization, we need *another* // authorization check so we can correctly redact the object namespaces below. - await this._securityExtension?.checkAuthorization({ + await this._securityExtension?.performAuthorization({ + actions: new Set(['find']), types: new Set(types), spaces: spacesToAuthorize, - actions: new Set(['find']), }) : undefined; @@ -1753,28 +1725,22 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { }), }; - const authorizationResult = await this._securityExtension?.checkAuthorization({ + const authorizationResult = await this._securityExtension?.performAuthorization({ + actions: new Set(['bulk_get']), types: new Set(typesAndSpaces.keys()), spaces: spacesToAuthorize, - actions: new Set(['bulk_get']), + enforceMap: typesAndSpaces, + auditCallback: (error) => { + for (const { type, id, error: bulkError } of result.saved_objects) { + if (!error && !!bulkError) continue; // Only log success events for objects that were actually found (and are being returned to the user) + this._securityExtension!.addAuditEvent({ + action: AuditAction.GET, + savedObject: { type, id }, + error, + }); + } + }, }); - if (authorizationResult) { - this._securityExtension!.enforceAuthorization({ - typesAndSpaces, - action: 'bulk_get', - typeMap: authorizationResult.typeMap, - auditCallback: (error) => { - for (const { type, id, error: bulkError } of result.saved_objects) { - if (!error && !!bulkError) continue; // Only log success events for objects that were actually found (and are being returned to the user) - this._securityExtension!.addAuditEvent({ - action: AuditAction.GET, - savedObject: { type, id }, - error, - }); - } - }, - }); - } return this.optionallyDecryptAndRedactBulkResult(result, authorizationResult?.typeMap); } @@ -1842,28 +1808,23 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { const spacesToEnforce = new Set([SavedObjectsUtils.namespaceIdToString(namespace)]); // Always check/enforce authZ for the active space const existingNamespaces = body?._source?.namespaces || []; - const authorizationResult = await this._securityExtension?.checkAuthorization({ + + const authorizationResult = await this._securityExtension?.performAuthorization({ + actions: new Set(['get']), types: new Set([type]), spaces: new Set([...spacesToEnforce, ...existingNamespaces]), // existing namespaces are included so we can later redact if necessary - actions: new Set(['get']), + enforceMap: new Map([[type, spacesToEnforce]]), + auditCallback: (error) => { + if (error) { + this._securityExtension!.addAuditEvent({ + action: AuditAction.GET, + savedObject: { type, id }, + error, + }); + } + // Audit event for success case is added separately below + }, }); - if (authorizationResult) { - this._securityExtension!.enforceAuthorization({ - typesAndSpaces: new Map([[type, spacesToEnforce]]), - action: 'get', - typeMap: authorizationResult.typeMap, - auditCallback: (error) => { - if (error) { - this._securityExtension!.addAuditEvent({ - action: AuditAction.GET, - savedObject: { type, id }, - error, - }); - } - // Audit event for success case is added separately below - }, - }); - } if ( !isFoundGetResponse(body) || @@ -1949,25 +1910,20 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { const spacesToEnforce = new Set([SavedObjectsUtils.namespaceIdToString(namespace)]); // Always check/enforce authZ for the active space const existingNamespaces = preflightResult?.savedObjectNamespaces || []; - const authorizationResult = await this._securityExtension?.checkAuthorization({ + + const authorizationResult = await this._securityExtension?.performAuthorization({ + actions: new Set(['update']), types: new Set([type]), spaces: new Set([...spacesToEnforce, ...existingNamespaces]), // existing namespaces are included so we can later redact if necessary - actions: new Set(['update']), + enforceMap: new Map([[type, spacesToEnforce]]), + auditCallback: (error) => + this._securityExtension!.addAuditEvent({ + action: AuditAction.UPDATE, + savedObject: { type, id }, + error, + ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the update/upsert operation has not occurred yet + }), }); - if (authorizationResult) { - this._securityExtension!.enforceAuthorization({ - typesAndSpaces: new Map([[type, spacesToEnforce]]), - action: 'update', - typeMap: authorizationResult.typeMap, - auditCallback: (error) => - this._securityExtension!.addAuditEvent({ - action: AuditAction.UPDATE, - savedObject: { type, id }, - error, - ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the update/upsert operation has not occurred yet - }), - }); - } if ( preflightResult?.checkResult === 'found_outside_namespace' || @@ -2236,28 +2192,22 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { } } - const authorizationResult = await this._securityExtension?.checkAuthorization({ + const authorizationResult = await this._securityExtension?.performAuthorization({ + actions: new Set(['bulk_update']), types: new Set(typesAndSpaces.keys()), spaces: spacesToAuthorize, - actions: new Set(['bulk_update']), + enforceMap: typesAndSpaces, + auditCallback: (error) => { + for (const { value } of validObjects) { + this._securityExtension!.addAuditEvent({ + action: AuditAction.UPDATE, + savedObject: { type: value.type, id: value.id }, + error, + ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the update operation has not occurred yet + }); + } + }, }); - if (authorizationResult) { - this._securityExtension!.enforceAuthorization({ - typesAndSpaces, - action: 'bulk_update', - typeMap: authorizationResult.typeMap, - auditCallback: (error) => { - for (const { value } of validObjects) { - this._securityExtension!.addAuditEvent({ - action: AuditAction.UPDATE, - savedObject: { type: value.type, id: value.id }, - error, - ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the update operation has not occurred yet - }); - } - }, - }); - } let bulkUpdateRequestIndexCounter = 0; const bulkUpdateParams: object[] = []; @@ -2416,25 +2366,19 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { // TODO: Improve authorization and auditing (https://github.com/elastic/kibana/issues/135259) const spaces = new Set([SavedObjectsUtils.namespaceIdToString(namespace)]); // Always check/enforce authZ for the active space - const authorizationResult = await this._securityExtension?.checkAuthorization({ + await this._securityExtension?.performAuthorization({ + actions: new Set(['delete']), types: new Set([type]), spaces, - actions: new Set(['delete']), + enforceMap: new Map([[type, spaces]]), + auditCallback: (error) => + this._securityExtension!.addAuditEvent({ + action: AuditAction.REMOVE_REFERENCES, + savedObject: { type, id }, + error, + ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the updateByQuery operation has not occurred yet + }), }); - if (authorizationResult) { - this._securityExtension!.enforceAuthorization({ - typesAndSpaces: new Map([[type, spaces]]), - action: 'delete', - typeMap: authorizationResult.typeMap, - auditCallback: (error) => - this._securityExtension!.addAuditEvent({ - action: AuditAction.REMOVE_REFERENCES, - savedObject: { type, id }, - error, - ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the updateByQuery operation has not occurred yet - }), - }); - } const allTypes = this._registry.getAllTypes().map((t) => t.name); @@ -2707,10 +2651,10 @@ export class SavedObjectsRepository implements ISavedObjectsRepository { if (!disableExtensions && this._securityExtension) { const spaces = new Set(namespaces); - const preAuthorizationResult = await this._securityExtension.checkAuthorization({ + const preAuthorizationResult = await this._securityExtension?.performAuthorization({ + actions: new Set(['open_point_in_time']), types: new Set(types), spaces, - actions: new Set(['open_point_in_time']), }); if (preAuthorizationResult.status === 'unauthorized') { // If the user is unauthorized to find *anything* they requested, return an empty response diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.test.ts index 821d24b9c3b4f..6016ef84fdc40 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.test.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.test.ts @@ -20,6 +20,8 @@ import type { SavedObjectsUpdateObjectsSpacesObject } from '@kbn/core-saved-obje import { SavedObjectsErrorHelpers, ALL_NAMESPACES_STRING, + setsAreEqual, + setMapsAreEqual, } from '@kbn/core-saved-objects-utils-server'; import { SavedObjectsSerializer } from '@kbn/core-saved-objects-base-server-internal'; import { typeRegistryMock } from '@kbn/core-saved-objects-base-server-mocks'; @@ -27,15 +29,10 @@ import type { UpdateObjectsSpacesParams } from './update_objects_spaces'; import { updateObjectsSpaces } from './update_objects_spaces'; import { AuditAction, type ISavedObjectsSecurityExtension } from '@kbn/core-saved-objects-server'; import { - authMap, checkAuthError, enforceError, - typeMapsAreEqual, - setsAreEqual, - setupCheckAuthorized, - setupCheckUnauthorized, - setupEnforceFailure, - setupEnforceSuccess, + setupPerformAuthFullyAuthorized, + setupPerformAuthEnforceFailure, setupRedactPassthrough, } from '../test_helpers/repository.test.common'; import { savedObjectsExtensionsMock } from '../mocks/saved_objects_extensions.mock'; @@ -654,6 +651,11 @@ describe('#updateObjectsSpaces', () => { let mockSecurityExt: jest.Mocked; let params: UpdateObjectsSpacesParams; + afterEach(() => { + mockSecurityExt.performAuthorization.mockClear(); + mockSecurityExt.redactNamespaces.mockClear(); + }); + describe(`errors`, () => { beforeEach(() => { const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1' }; @@ -666,8 +668,7 @@ describe('#updateObjectsSpaces', () => { }); test(`propagates error from es client bulk get`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); const error = SavedObjectsErrorHelpers.createBadRequestError('OOPS!'); @@ -681,30 +682,25 @@ describe('#updateObjectsSpaces', () => { await expect(updateObjectsSpaces(params)).rejects.toThrow(error); }); - test(`propagates decorated error when checkAuthorization rejects promise`, async () => { - mockSecurityExt.checkAuthorization.mockRejectedValueOnce(checkAuthError); + test(`propagates decorated error when performAuthorization rejects promise`, async () => { + mockSecurityExt.performAuthorization.mockRejectedValueOnce(checkAuthError); await expect(updateObjectsSpaces(params)).rejects.toThrow(checkAuthError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).not.toHaveBeenCalled(); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`propagates decorated error when unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect(updateObjectsSpaces(params)).rejects.toThrow(enforceError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); }); test(`adds audit event when not unauthorized`, async () => { - setupCheckUnauthorized(mockSecurityExt); - setupEnforceFailure(mockSecurityExt); + setupPerformAuthEnforceFailure(mockSecurityExt); await expect(updateObjectsSpaces(params)).rejects.toThrow(enforceError); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledTimes(1); expect(mockSecurityExt.addAuditEvent).toHaveBeenCalledWith({ @@ -717,8 +713,7 @@ describe('#updateObjectsSpaces', () => { }); test(`returns error from es client bulk operation`, async () => { - setupCheckAuthorized(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); mockGetBulkOperationError.mockReset(); @@ -761,51 +756,37 @@ describe('#updateObjectsSpaces', () => { { found: true, namespaces: [EXISTING_SPACE] } // result for obj4 -- will be updated to remove EXISTING_SPACE and add otherSpace ); mockBulkResults({ error: false }, { error: false }, { error: false }); // results for obj2, obj3, and obj4 - setupCheckAuthorized(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); }); - test(`calls checkAuthorization with type, actions, and namespaces`, async () => { + test(`calls performAuthorization with correct actions, types, spaces, and enforce map`, async () => { await updateObjectsSpaces(params); expect(client.bulk).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['share_to_space']); const expectedSpaces = new Set([defaultSpace, otherSpace, EXISTING_SPACE]); const expectedTypes = new Set([SHAREABLE_OBJ_TYPE]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set( + SHAREABLE_OBJ_TYPE, + new Set([defaultSpace, otherSpace, EXISTING_SPACE]) + ); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); - }); - - test(`calls enforceAuthorization with action, type map, and auth map`, async () => { - await updateObjectsSpaces(params); - - expect(client.bulk).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.enforceAuthorization).toHaveBeenCalledWith( - expect.objectContaining({ - action: 'share_to_space', - }) - ); - const expectedTypesAndSpaces = new Map([ - [SHAREABLE_OBJ_TYPE, new Set([defaultSpace, EXISTING_SPACE, otherSpace])], - ]); - - const { typesAndSpaces: actualTypesAndSpaces, typeMap: actualTypeMap } = - mockSecurityExt.enforceAuthorization.mock.calls[0][0]; - - expect(typeMapsAreEqual(actualTypesAndSpaces, expectedTypesAndSpaces)).toBeTruthy(); - expect(actualTypeMap).toBe(authMap); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toEqual(expect.objectContaining({ allowGlobalResource: true })); }); test(`adds audit event per object when successful`, async () => { @@ -844,55 +825,72 @@ describe('#updateObjectsSpaces', () => { { found: true, namespaces: [EXISTING_SPACE] } // result for obj4 -- will be updated to remove EXISTING_SPACE and add otherSpace ); mockBulkResults({ error: false }, { error: false }, { error: false }); // results for obj2, obj3, and obj4 - setupCheckAuthorized(mockSecurityExt); - setupEnforceSuccess(mockSecurityExt); + setupPerformAuthFullyAuthorized(mockSecurityExt); setupRedactPassthrough(mockSecurityExt); }; - test(`calls checkAuthorization with '*' when spacesToAdd includes '*'`, async () => { + test(`calls performAuthorization with '*' when spacesToAdd includes '*'`, async () => { const spacesToAdd = ['*']; const spacesToRemove = [otherSpace]; setupForAllSpaces(spacesToAdd, spacesToRemove); await updateObjectsSpaces(params); expect(client.bulk).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['share_to_space']); const expectedSpaces = new Set(['*', defaultSpace, otherSpace, EXISTING_SPACE]); const expectedTypes = new Set([SHAREABLE_OBJ_TYPE]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set( + SHAREABLE_OBJ_TYPE, + new Set([defaultSpace, otherSpace, ...spacesToAdd]) + ); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toEqual(expect.objectContaining({ allowGlobalResource: true })); }); - test(`calls checkAuthorization with '*' when spacesToRemove includes '*'`, async () => { + test(`calls performAuthorization with '*' when spacesToRemove includes '*'`, async () => { const spacesToAdd = [otherSpace]; const spacesToRemove = ['*']; setupForAllSpaces(spacesToAdd, spacesToRemove); await updateObjectsSpaces(params); expect(client.bulk).toHaveBeenCalledTimes(1); - expect(mockSecurityExt.checkAuthorization).toHaveBeenCalledTimes(1); + expect(mockSecurityExt.performAuthorization).toHaveBeenCalledTimes(1); const expectedActions = new Set(['share_to_space']); const expectedSpaces = new Set(['*', defaultSpace, otherSpace, EXISTING_SPACE]); const expectedTypes = new Set([SHAREABLE_OBJ_TYPE]); + const expectedEnforceMap = new Map>(); + expectedEnforceMap.set( + SHAREABLE_OBJ_TYPE, + new Set([defaultSpace, otherSpace, ...spacesToRemove]) + ); const { actions: actualActions, spaces: actualSpaces, types: actualTypes, - } = mockSecurityExt.checkAuthorization.mock.calls[0][0]; + enforceMap: actualEnforceMap, + options: actualOptions, + } = mockSecurityExt.performAuthorization.mock.calls[0][0]; expect(setsAreEqual(actualActions, expectedActions)).toBeTruthy(); expect(setsAreEqual(actualSpaces, expectedSpaces)).toBeTruthy(); expect(setsAreEqual(actualTypes, expectedTypes)).toBeTruthy(); + expect(setMapsAreEqual(actualEnforceMap, expectedEnforceMap)).toBeTruthy(); + expect(actualOptions).toEqual(expect.objectContaining({ allowGlobalResource: true })); }); }); }); diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.ts index f4afe7cf96961..3144460270178 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/update_objects_spaces.ts @@ -204,33 +204,27 @@ export async function updateObjectsSpaces({ } } - const authorizationResult = await securityExtension?.checkAuthorization({ - types: new Set(typesAndSpaces.keys()), - spaces: spacesToAuthorize, - actions: new Set(['share_to_space']), + const authorizationResult = await securityExtension?.performAuthorization({ // If a user tries to share/unshare an object to/from '*', they need to have 'share_to_space' privileges for the Global Resource (e.g., // All privileges for All Spaces). + actions: new Set(['share_to_space']), + types: new Set(typesAndSpaces.keys()), + spaces: spacesToAuthorize, + enforceMap: typesAndSpaces, + auditCallback: (error) => { + for (const { value } of validObjects) { + securityExtension!.addAuditEvent({ + action: AuditAction.UPDATE_OBJECTS_SPACES, + savedObject: { type: value.type, id: value.id }, + addToSpaces, + deleteFromSpaces, + error, + ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the update operation has not occurred yet + }); + } + }, options: { allowGlobalResource: true }, }); - if (authorizationResult) { - securityExtension!.enforceAuthorization({ - typesAndSpaces, - action: 'share_to_space', - typeMap: authorizationResult.typeMap, - auditCallback: (error) => { - for (const { value } of validObjects) { - securityExtension!.addAuditEvent({ - action: AuditAction.UPDATE_OBJECTS_SPACES, - savedObject: { type: value.type, id: value.id }, - addToSpaces, - deleteFromSpaces, - error, - ...(!error && { outcome: 'unknown' }), // If authorization was a success, the outcome is unknown because the update operation has not occurred yet - }); - } - }, - }); - } const time = new Date().toISOString(); let bulkOperationRequestIndexCounter = 0; diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/saved_objects_extensions.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/saved_objects_extensions.mock.ts index f4308ee6254c7..fe775dd0cf945 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/saved_objects_extensions.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/mocks/saved_objects_extensions.mock.ts @@ -20,7 +20,7 @@ const createEncryptionExtension = (): jest.Mocked => ({ - checkAuthorization: jest.fn(), + performAuthorization: jest.fn(), enforceAuthorization: jest.fn(), addAuditEvent: jest.fn(), redactNamespaces: jest.fn(), diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts index 600b1e967c677..396686b735e2f 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/test_helpers/repository.test.common.ts @@ -9,12 +9,12 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { schema } from '@kbn/config-schema'; import { loggerMock } from '@kbn/logging-mocks'; -import { isEqual } from 'lodash'; import { Payload } from 'elastic-apm-node'; import { AuthorizationTypeEntry, - EnforceAuthorizationParams, + CheckAuthorizationResult, ISavedObjectsSecurityExtension, + PerformAuthorizationParams, SavedObjectsMappingProperties, SavedObjectsRawDocSource, SavedObjectsType, @@ -235,49 +235,47 @@ export const enforceError = SavedObjectsErrorHelpers.decorateForbiddenError( 'User lacks privileges' ); -export const setupCheckAuthorized = ( +export const setupPerformAuthFullyAuthorized = ( mockSecurityExt: jest.Mocked ) => { - mockSecurityExt.checkAuthorization.mockResolvedValue({ - status: 'fully_authorized', - typeMap: authMap, - }); -}; - -export const setupCheckPartiallyAuthorized = ( - mockSecurityExt: jest.Mocked -) => { - mockSecurityExt.checkAuthorization.mockResolvedValue({ - status: 'partially_authorized', - typeMap: authMap, - }); + mockSecurityExt.performAuthorization.mockImplementation( + (params: PerformAuthorizationParams): Promise> => { + const { auditCallback } = params; + auditCallback?.(undefined); + return Promise.resolve({ status: 'fully_authorized', typeMap: authMap }); + } + ); }; -export const setupCheckUnauthorized = ( +export const setupPerformAuthPartiallyAuthorized = ( mockSecurityExt: jest.Mocked ) => { - mockSecurityExt.checkAuthorization.mockResolvedValue({ - status: 'unauthorized', - typeMap: new Map([]), - }); + mockSecurityExt.performAuthorization.mockImplementation( + (params: PerformAuthorizationParams): Promise> => { + const { auditCallback } = params; + auditCallback?.(undefined); + return Promise.resolve({ status: 'partially_authorized', typeMap: authMap }); + } + ); }; -export const setupEnforceSuccess = ( +export const setupPerformAuthUnauthorized = ( mockSecurityExt: jest.Mocked ) => { - mockSecurityExt.enforceAuthorization.mockImplementation( - (params: EnforceAuthorizationParams) => { + mockSecurityExt.performAuthorization.mockImplementation( + (params: PerformAuthorizationParams): Promise> => { const { auditCallback } = params; auditCallback?.(undefined); + return Promise.resolve({ status: 'unauthorized', typeMap: new Map([]) }); } ); }; -export const setupEnforceFailure = ( +export const setupPerformAuthEnforceFailure = ( mockSecurityExt: jest.Mocked ) => { - mockSecurityExt.enforceAuthorization.mockImplementation( - (params: EnforceAuthorizationParams) => { + mockSecurityExt.performAuthorization.mockImplementation( + (params: PerformAuthorizationParams) => { const { auditCallback } = params; auditCallback?.(enforceError); throw enforceError; @@ -850,27 +848,6 @@ export const getSuccess = async ( return result; }; -export function setsAreEqual(setA: Set, setB: Set) { - return isEqual(Array(setA).sort(), Array(setB).sort()); -} - -export function typeMapsAreEqual(mapA: Map>, mapB: Map>) { - return ( - mapA.size === mapB.size && - Array.from(mapA.keys()).every((key) => setsAreEqual(mapA.get(key)!, mapB.get(key)!)) - ); -} - -export function namespaceMapsAreEqual( - mapA: Map, - mapB: Map -) { - return ( - mapA.size === mapB.size && - Array.from(mapA.keys()).every((key) => isEqual(mapA.get(key)?.sort(), mapB.get(key)?.sort())) - ); -} - export const getMockEsBulkDeleteResponse = ( registry: SavedObjectTypeRegistry, objects: TypeIdTuple[], diff --git a/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/saved_objects_extensions.mock.ts b/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/saved_objects_extensions.mock.ts index f4308ee6254c7..fe775dd0cf945 100644 --- a/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/saved_objects_extensions.mock.ts +++ b/packages/core/saved-objects/core-saved-objects-api-server-mocks/src/saved_objects_extensions.mock.ts @@ -20,7 +20,7 @@ const createEncryptionExtension = (): jest.Mocked => ({ - checkAuthorization: jest.fn(), + performAuthorization: jest.fn(), enforceAuthorization: jest.fn(), addAuditEvent: jest.fn(), redactNamespaces: jest.fn(), diff --git a/packages/core/saved-objects/core-saved-objects-server/index.ts b/packages/core/saved-objects/core-saved-objects-server/index.ts index c96d40ca7a72f..f8791a1efbe5a 100644 --- a/packages/core/saved-objects/core-saved-objects-server/index.ts +++ b/packages/core/saved-objects/core-saved-objects-server/index.ts @@ -72,6 +72,7 @@ export type { } from './src/extensions/encryption'; export type { CheckAuthorizationParams, + PerformAuthorizationParams, AuthorizationTypeEntry, AuthorizationTypeMap, CheckAuthorizationResult, diff --git a/packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts b/packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts index 2d6df725068be..4364bbbf2ad8f 100644 --- a/packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts +++ b/packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts @@ -9,6 +9,43 @@ import type { SavedObject } from '@kbn/core-saved-objects-common'; import type { EcsEventOutcome } from '@kbn/ecs'; +/** + * The PerformAuthorizationParams interface contains settings for checking + * & enforcing authorization via the ISavedObjectsSecurityExtension. + */ +export interface PerformAuthorizationParams { + /** + * A set of actions to check. + */ + actions: Set; + /** + * A set of types to check. + */ + types: Set; + /** + * A set of spaces to check (types to check comes from the typesAndSpaces map). + */ + spaces: Set; + /** + * A map of types (key) to spaces (value) that will be affected by the action(s). + * If undefined, enforce with be bypassed. + */ + enforceMap?: Map>; + /** + * A callback intended to handle adding audit events in + * both error (unauthorized), or success (authorized) + * cases + */ + auditCallback?: (error?: Error) => void; + /** + * Authorization options + * allowGlobalResource - whether or not to allow global resources, false if options are undefined + */ + options?: { + allowGlobalResource: boolean; + }; +} + /** * The CheckAuthorizationParams interface contains settings for checking * authorization via the ISavedObjectsSecurityExtension. @@ -178,12 +215,12 @@ export interface RedactNamespacesParams { */ export interface ISavedObjectsSecurityExtension { /** - * Checks authorization of actions on specified types in specified spaces. - * @param params - types, spaces, and actions to check + * Performs authorization (check & enforce) of actions on specified types in specified spaces. + * @param params - actions, types & spaces map, audit callback, options (enforce bypassed if enforce map is undefined) * @returns CheckAuthorizationResult - the resulting authorization level and authorization map */ - checkAuthorization: ( - params: CheckAuthorizationParams + performAuthorization: ( + params: PerformAuthorizationParams ) => Promise>; /** diff --git a/packages/core/saved-objects/core-saved-objects-utils-server/index.ts b/packages/core/saved-objects/core-saved-objects-utils-server/index.ts index ee85e185a68b2..bae7583ae1e92 100644 --- a/packages/core/saved-objects/core-saved-objects-utils-server/index.ts +++ b/packages/core/saved-objects/core-saved-objects-utils-server/index.ts @@ -15,3 +15,5 @@ export { FIND_DEFAULT_PAGE, FIND_DEFAULT_PER_PAGE, } from './src/saved_objects_utils'; + +export { setsAreEqual, arrayMapsAreEqual, setMapsAreEqual } from './src/saved_objects_test_utils'; diff --git a/packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.test.ts b/packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.test.ts new file mode 100644 index 0000000000000..a82095060070e --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.test.ts @@ -0,0 +1,160 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { arrayMapsAreEqual, setMapsAreEqual, setsAreEqual } from './saved_objects_test_utils'; + +describe('savedObjects/testUtils', () => { + describe('#setsAreEqual', () => { + const setA = new Set(['1', '2', '3']); + const setB = new Set(['1', '2']); + const setC = new Set(['1', '3', '4']); + const setD = new Set(['2', '3', '1']); + + describe('inequal', () => { + it('should return false if the sets are not the same size', () => { + expect(setsAreEqual(setA, setB)).toBeFalsy(); + expect(setsAreEqual(setB, setA)).toBeFalsy(); + expect(setsAreEqual(setA, new Set())).toBeFalsy(); + expect(setsAreEqual(new Set(), setA)).toBeFalsy(); + }); + + it('should return false if the sets do not have the same values', () => { + expect(setsAreEqual(setA, setC)).toBeFalsy(); + expect(setsAreEqual(setC, setA)).toBeFalsy(); + }); + }); + + describe('equal', () => { + it('should return true if the sets are exactly the same', () => { + expect(setsAreEqual(setA, setD)).toBeTruthy(); + expect(setsAreEqual(setD, setA)).toBeTruthy(); + expect(setsAreEqual(new Set(), new Set())).toBeTruthy(); + }); + }); + }); + + describe('#arrayMapsAreEqual', () => { + const mapA = new Map(); + mapA.set('a', ['1', '2', '3']); + mapA.set('b', ['1', '2']); + + const mapB = new Map(); + mapB.set('a', ['1', '2', '3']); + + const mapC = new Map(); + mapC.set('a', ['1', '2', '3']); + mapC.set('c', ['1', '2']); + + const mapD = new Map(); + mapD.set('a', ['1', '2', '3']); + mapD.set('b', ['1', '3']); + + const mapE = new Map(); + mapE.set('b', ['2', '1']); + mapE.set('a', ['3', '1', '2']); + + const mapF = new Map(); + mapF.set('a', ['1', '2', '3']); + mapF.set('b', undefined); + + const mapG = new Map(); + mapG.set('b', undefined); + mapG.set('a', ['3', '1', '2']); + + const mapH = new Map(); + mapF.set('a', ['1', '2', '3']); + mapF.set('b', []); + + const mapI = new Map(); + mapG.set('b', []); + mapG.set('a', ['3', '1', '2']); + + describe('inequal', () => { + it('should return false if the maps are not the same size', () => { + expect(arrayMapsAreEqual(mapA, mapB)).toBeFalsy(); + expect(arrayMapsAreEqual(mapB, mapA)).toBeFalsy(); + expect(arrayMapsAreEqual(mapA, new Map())).toBeFalsy(); + expect(arrayMapsAreEqual(new Map(), mapA)).toBeFalsy(); + }); + + it('should return false if the maps do not have the same keys', () => { + expect(arrayMapsAreEqual(mapA, mapC)).toBeFalsy(); + expect(arrayMapsAreEqual(mapC, mapA)).toBeFalsy(); + }); + + it('should return false if the maps do not have the same values', () => { + expect(arrayMapsAreEqual(mapA, mapD)).toBeFalsy(); + expect(arrayMapsAreEqual(mapD, mapA)).toBeFalsy(); + expect(arrayMapsAreEqual(mapA, mapF)).toBeFalsy(); + expect(arrayMapsAreEqual(mapF, mapA)).toBeFalsy(); + expect(arrayMapsAreEqual(mapA, mapH)).toBeFalsy(); + expect(arrayMapsAreEqual(mapH, mapA)).toBeFalsy(); + }); + }); + + describe('equal', () => { + it('should return true if the maps are exactly the same', () => { + expect(arrayMapsAreEqual(mapA, mapE)).toBeTruthy(); + expect(arrayMapsAreEqual(mapE, mapA)).toBeTruthy(); + expect(arrayMapsAreEqual(new Map(), new Map())).toBeTruthy(); + expect(arrayMapsAreEqual(mapF, mapG)).toBeTruthy(); + expect(arrayMapsAreEqual(mapG, mapF)).toBeTruthy(); + expect(arrayMapsAreEqual(mapH, mapI)).toBeTruthy(); + expect(arrayMapsAreEqual(mapI, mapH)).toBeTruthy(); + }); + }); + }); + + describe('#setMapsAreEqual', () => { + const mapA = new Map>(); + mapA.set('a', new Set(['1', '2', '3'])); + mapA.set('b', new Set(['1', '2'])); + + const mapB = new Map>(); + mapB.set('a', new Set(['1', '2', '3'])); + + const mapC = new Map>(); + mapC.set('a', new Set(['1', '2', '3'])); + mapC.set('c', new Set(['1', '2'])); + + const mapD = new Map>(); + mapD.set('a', new Set(['1', '2', '3'])); + mapD.set('b', new Set(['1', '3'])); + + const mapE = new Map>(); + mapE.set('b', new Set(['2', '1'])); + mapE.set('a', new Set(['3', '1', '2'])); + + describe('inequal', () => { + it('should return false if the maps are not the same size', () => { + expect(setMapsAreEqual(mapA, mapB)).toBeFalsy(); + expect(setMapsAreEqual(mapB, mapA)).toBeFalsy(); + expect(setMapsAreEqual(mapA, new Map())).toBeFalsy(); + expect(setMapsAreEqual(new Map(), mapA)).toBeFalsy(); + }); + + it('should return false if the maps do not have the same keys', () => { + expect(setMapsAreEqual(mapA, mapC)).toBeFalsy(); + expect(setMapsAreEqual(mapC, mapA)).toBeFalsy(); + }); + + it('should return false if the maps do not have the same values', () => { + expect(setMapsAreEqual(mapA, mapD)).toBeFalsy(); + expect(setMapsAreEqual(mapD, mapA)).toBeFalsy(); + }); + }); + + describe('equal', () => { + it('should return true if the maps are exactly the same', () => { + expect(setMapsAreEqual(mapA, mapE)).toBeTruthy(); + expect(setMapsAreEqual(mapE, mapA)).toBeTruthy(); + expect(setMapsAreEqual(new Map(), new Map())).toBeTruthy(); + }); + }); + }); +}); diff --git a/packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.ts b/packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.ts new file mode 100644 index 0000000000000..16cf2878c21c9 --- /dev/null +++ b/packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.ts @@ -0,0 +1,70 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { isEqual } from 'lodash'; + +/** + * Determines if a given Set is equal to another given Set. Set types must be the same, and comparable. + * + * @param setA The first Set to compare + * @param setB The second Set to compare + * @returns {boolean} True if Set A is equal to Set B + */ +export function setsAreEqual(setA: Set, setB: Set) { + if (setA.size !== setB.size) return false; + + for (const element of setA) { + if (!setB.has(element)) { + return false; + } + } + + return true; +} + +/** + * Determines if a given map of arrays is equal to another given map of arrays. + * Used for comparing namespace maps in saved object repo/security extension tests. + * + * @param mapA The first map to compare + * @param mapB The second map to compare + * @returns {boolean} True if map A is equal to map B + */ +export function arrayMapsAreEqual(mapA: Map, mapB: Map) { + if (mapA?.size !== mapB?.size) return false; + + for (const [key, valueA] of mapA!) { + const valueB = mapB?.get(key); + if (valueA?.length !== valueB?.length) return false; + if (!isEqual(valueA?.sort(), valueB?.sort())) return false; + } + + return true; +} + +/** + * Determines if a given Map of Sets is equal to another given Map of Sets. + * Used for comparing typeMaps and enforceMaps in saved object repo/security extension tests. + * + * @param mapA The first map to compare + * @param mapB The second map to compare + * @returns {boolean} True if map A is equal to map B + */ +export function setMapsAreEqual( + mapA: Map> | undefined, + mapB: Map> | undefined +) { + if (mapA?.size !== mapB?.size) return false; + + for (const [key, valueA] of mapA!) { + const valueB = mapB?.get(key); + if (!valueB || !setsAreEqual(valueA, valueB)) return false; + } + + return true; +} diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 054887bed7b86..3b4f860093c09 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -356,7 +356,7 @@ export type { SavedObjectsRequestHandlerContext, EncryptedObjectDescriptor, ISavedObjectsEncryptionExtension, - CheckAuthorizationParams, + PerformAuthorizationParams, AuthorizationTypeEntry, AuthorizationTypeMap, CheckAuthorizationResult, diff --git a/x-pack/plugins/security/server/saved_objects/saved_objects_security_extension.test.ts b/x-pack/plugins/security/server/saved_objects/saved_objects_security_extension.test.ts index 3c6ff01aea920..1aad389855c10 100644 --- a/x-pack/plugins/security/server/saved_objects/saved_objects_security_extension.test.ts +++ b/x-pack/plugins/security/server/saved_objects/saved_objects_security_extension.test.ts @@ -34,234 +34,6 @@ function setup() { return { actions, auditLogger, errors, checkPrivileges, securityExtension }; } -describe('#checkAuthorization', () => { - // These arguments are used for all unit tests below - const types = new Set(['a', 'b', 'c']); - const spaces = new Set(['x', 'y']); - const actions = new Set(['foo', 'bar']); - - const fullyAuthorizedCheckPrivilegesResponse = { - hasAllRequested: true, - privileges: { - kibana: [ - { privilege: 'mock-saved_object:a/foo', authorized: true }, - { privilege: 'mock-saved_object:a/bar', authorized: true }, - { privilege: 'login:', authorized: true }, - { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: true }, - { resource: 'x', privilege: 'mock-saved_object:b/bar', authorized: true }, - { resource: 'x', privilege: 'mock-saved_object:c/foo', authorized: true }, - { resource: 'x', privilege: 'mock-saved_object:c/bar', authorized: true }, - { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: true }, - { resource: 'y', privilege: 'mock-saved_object:b/bar', authorized: true }, - { resource: 'y', privilege: 'mock-saved_object:c/foo', authorized: true }, - { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: true }, - ], - }, - } as CheckPrivilegesResponse; - - test('calls checkPrivileges with expected privilege actions and namespaces', async () => { - const { securityExtension, checkPrivileges } = setup(); - checkPrivileges.mockResolvedValue(fullyAuthorizedCheckPrivilegesResponse); // Return any well-formed response to avoid an unhandled error - - await securityExtension.checkAuthorization({ types, spaces, actions }); - expect(checkPrivileges).toHaveBeenCalledWith( - [ - 'mock-saved_object:a/foo', - 'mock-saved_object:a/bar', - 'mock-saved_object:b/foo', - 'mock-saved_object:b/bar', - 'mock-saved_object:c/foo', - 'mock-saved_object:c/bar', - 'login:', - ], - [...spaces] - ); - }); - - test('throws an error when `types` is empty', async () => { - const { securityExtension, checkPrivileges } = setup(); - - await expect( - securityExtension.checkAuthorization({ types: new Set(), spaces, actions }) - ).rejects.toThrowError('No types specified for authorization check'); - expect(checkPrivileges).not.toHaveBeenCalled(); - }); - - test('throws an error when `spaces` is empty', async () => { - const { securityExtension, checkPrivileges } = setup(); - - await expect( - securityExtension.checkAuthorization({ types, spaces: new Set(), actions }) - ).rejects.toThrowError('No spaces specified for authorization check'); - expect(checkPrivileges).not.toHaveBeenCalled(); - }); - - test('throws an error when `actions` is empty', async () => { - const { securityExtension, checkPrivileges } = setup(); - - await expect( - securityExtension.checkAuthorization({ types, spaces, actions: new Set([]) }) - ).rejects.toThrowError('No actions specified for authorization check'); - expect(checkPrivileges).not.toHaveBeenCalled(); - }); - - test('throws an error when privilege check fails', async () => { - const { securityExtension, checkPrivileges } = setup(); - checkPrivileges.mockRejectedValue(new Error('Oh no!')); - - await expect( - securityExtension.checkAuthorization({ types, spaces, actions }) - ).rejects.toThrowError('Oh no!'); - }); - - test('fully authorized', async () => { - const { securityExtension, checkPrivileges } = setup(); - checkPrivileges.mockResolvedValue(fullyAuthorizedCheckPrivilegesResponse); - - const result = await securityExtension.checkAuthorization({ types, spaces, actions }); - expect(result).toEqual({ - status: 'fully_authorized', - typeMap: new Map() - .set('a', { - foo: { isGloballyAuthorized: true, authorizedSpaces: [] }, - bar: { isGloballyAuthorized: true, authorizedSpaces: [] }, - // Technically, 'login:' is not a saved object action, it is a Kibana privilege -- however, we include it in the `typeMap` results - // for ease of use with the `redactNamespaces` function. The user is never actually authorized to "login" for a given object type, - // they are authorized to log in on a per-space basis, and this is applied to each object type in the typeMap result accordingly. - ['login:']: { isGloballyAuthorized: true, authorizedSpaces: [] }, - }) - .set('b', { - foo: { authorizedSpaces: ['x', 'y'] }, - bar: { authorizedSpaces: ['x', 'y'] }, - ['login:']: { isGloballyAuthorized: true, authorizedSpaces: [] }, - }) - .set('c', { - foo: { authorizedSpaces: ['x', 'y'] }, - bar: { authorizedSpaces: ['x', 'y'] }, - ['login:']: { isGloballyAuthorized: true, authorizedSpaces: [] }, - }), - }); - }); - - test('partially authorized', async () => { - const { securityExtension, checkPrivileges } = setup(); - checkPrivileges.mockResolvedValue({ - hasAllRequested: false, - privileges: { - kibana: [ - // For type 'a', the user is authorized to use 'foo' action but not 'bar' action (all spaces) - // For type 'b', the user is authorized to use 'foo' action but not 'bar' action (both spaces) - // For type 'c', the user is authorized to use both actions in space 'x' but not space 'y' - { privilege: 'mock-saved_object:a/foo', authorized: true }, - { privilege: 'mock-saved_object:a/bar', authorized: false }, - { privilege: 'mock-saved_object:a/bar', authorized: true }, // fail-secure check - { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: true }, - { resource: 'x', privilege: 'mock-saved_object:b/bar', authorized: false }, - { resource: 'x', privilege: 'mock-saved_object:c/foo', authorized: true }, - { privilege: 'mock-saved_object:c/foo', authorized: false }, // inverse fail-secure check - { resource: 'x', privilege: 'mock-saved_object:c/bar', authorized: true }, - { resource: 'x', privilege: 'login:', authorized: true }, - { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: true }, - { resource: 'y', privilege: 'mock-saved_object:b/bar', authorized: false }, - { resource: 'y', privilege: 'mock-saved_object:c/foo', authorized: false }, - { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: false }, - { privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check - { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check - { resource: 'y', privilege: 'login:', authorized: true }, - // The fail-secure checks are a contrived scenario, as we *shouldn't* get both an unauthorized and authorized result for a given resource... - // However, in case we do, we should fail-secure (authorized + unauthorized = unauthorized) - ], - }, - } as CheckPrivilegesResponse); - - const result = await securityExtension.checkAuthorization({ types, spaces, actions }); - expect(result).toEqual({ - status: 'partially_authorized', - typeMap: new Map() - .set('a', { - foo: { isGloballyAuthorized: true, authorizedSpaces: [] }, - ['login:']: { authorizedSpaces: ['x', 'y'] }, - }) - .set('b', { - foo: { authorizedSpaces: ['x', 'y'] }, - ['login:']: { authorizedSpaces: ['x', 'y'] }, - }) - .set('c', { - foo: { authorizedSpaces: ['x'] }, - bar: { authorizedSpaces: ['x'] }, - ['login:']: { authorizedSpaces: ['x', 'y'] }, - }), - }); - }); - - test('unauthorized', async () => { - const { securityExtension, checkPrivileges } = setup(); - checkPrivileges.mockResolvedValue({ - hasAllRequested: false, - privileges: { - kibana: [ - { privilege: 'mock-saved_object:a/foo', authorized: false }, - { privilege: 'mock-saved_object:a/bar', authorized: false }, - { privilege: 'mock-saved_object:a/bar', authorized: true }, // fail-secure check - { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: false }, - { resource: 'x', privilege: 'mock-saved_object:b/bar', authorized: false }, - { resource: 'x', privilege: 'mock-saved_object:c/foo', authorized: false }, - { resource: 'x', privilege: 'mock-saved_object:c/bar', authorized: false }, - { resource: 'x', privilege: 'login:', authorized: false }, - { resource: 'x', privilege: 'login:', authorized: true }, // fail-secure check - { resource: 'y', privilege: 'mock-saved_object:a/foo', authorized: false }, - { resource: 'y', privilege: 'mock-saved_object:a/bar', authorized: false }, - { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: false }, - { resource: 'y', privilege: 'mock-saved_object:b/bar', authorized: false }, - { resource: 'y', privilege: 'mock-saved_object:c/foo', authorized: false }, - { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: false }, - { privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check - { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check - { resource: 'y', privilege: 'login:', authorized: true }, // should *not* result in a 'partially_authorized' status - // The fail-secure checks are a contrived scenario, as we *shouldn't* get both an unauthorized and authorized result for a given resource... - // However, in case we do, we should fail-secure (authorized + unauthorized = unauthorized) - ], - }, - } as CheckPrivilegesResponse); - - const result = await securityExtension.checkAuthorization({ types, spaces, actions }); - expect(result).toEqual({ - // The user is authorized to log into space Y, but they are not authorized to take any actions on any of the requested object types. - // Therefore, the status is 'unauthorized'. - status: 'unauthorized', - typeMap: new Map() - .set('a', { ['login:']: { authorizedSpaces: ['y'] } }) - .set('b', { ['login:']: { authorizedSpaces: ['y'] } }) - .set('c', { ['login:']: { authorizedSpaces: ['y'] } }), - }); - }); - - test('conflicting privilege failsafe', async () => { - const conflictingPrivilegesResponse = { - hasAllRequested: true, - privileges: { - kibana: [ - // redundant conflicting privileges for space X, type B, action Foo - { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: true }, - { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: false }, - { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: true }, - ], - }, - } as CheckPrivilegesResponse; - - const { securityExtension, checkPrivileges } = setup(); - checkPrivileges.mockResolvedValue(conflictingPrivilegesResponse); - - const result = await securityExtension.checkAuthorization({ types, spaces, actions }); - expect(result).toEqual({ - status: 'fully_authorized', - typeMap: new Map().set('b', { - foo: { authorizedSpaces: ['y'] }, // should NOT be authorized for conflicted privilege - }), - }); - }); -}); - describe('#enforceAuthorization', () => { test('fully authorized', () => { const { securityExtension } = setup(); @@ -367,6 +139,335 @@ describe('#enforceAuthorization', () => { }); }); +describe('#performAuthorization', () => { + describe('without enforce', () => { + // These arguments are used for all unit tests below + const types = new Set(['a', 'b', 'c']); + const spaces = new Set(['x', 'y']); + const actions = new Set(['foo', 'bar']); + + const fullyAuthorizedCheckPrivilegesResponse = { + hasAllRequested: true, + privileges: { + kibana: [ + { privilege: 'mock-saved_object:a/foo', authorized: true }, + { privilege: 'mock-saved_object:a/bar', authorized: true }, + { privilege: 'login:', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:b/bar', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:c/foo', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:c/bar', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:b/bar', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:c/foo', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: true }, + ], + }, + } as CheckPrivilegesResponse; + test('calls performPrivileges with expected privilege actions and namespaces', async () => { + const { securityExtension, checkPrivileges } = setup(); + checkPrivileges.mockResolvedValue(fullyAuthorizedCheckPrivilegesResponse); // Return any well-formed response to avoid an unhandled error + + await securityExtension.performAuthorization({ types, spaces, actions }); + expect(checkPrivileges).toHaveBeenCalledWith( + [ + 'mock-saved_object:a/foo', + 'mock-saved_object:a/bar', + 'mock-saved_object:b/foo', + 'mock-saved_object:b/bar', + 'mock-saved_object:c/foo', + 'mock-saved_object:c/bar', + 'login:', + ], + [...spaces] + ); + }); + + test('throws an error when `types` is empty', async () => { + const { securityExtension, checkPrivileges } = setup(); + + await expect( + securityExtension.performAuthorization({ types: new Set(), spaces, actions }) + ).rejects.toThrowError('No types specified for authorization check'); + expect(checkPrivileges).not.toHaveBeenCalled(); + }); + + test('throws an error when `spaces` is empty', async () => { + const { securityExtension, checkPrivileges } = setup(); + + await expect( + securityExtension.performAuthorization({ types, spaces: new Set(), actions }) + ).rejects.toThrowError('No spaces specified for authorization check'); + expect(checkPrivileges).not.toHaveBeenCalled(); + }); + + test('throws an error when `actions` is empty', async () => { + const { securityExtension, checkPrivileges } = setup(); + + await expect( + securityExtension.performAuthorization({ types, spaces, actions: new Set([]) }) + ).rejects.toThrowError('No actions specified for authorization check'); + expect(checkPrivileges).not.toHaveBeenCalled(); + }); + + test('throws an error when privilege check fails', async () => { + const { securityExtension, checkPrivileges } = setup(); + checkPrivileges.mockRejectedValue(new Error('Oh no!')); + + await expect( + securityExtension.performAuthorization({ types, spaces, actions }) + ).rejects.toThrowError('Oh no!'); + }); + + test('fully authorized', async () => { + const { securityExtension, checkPrivileges } = setup(); + checkPrivileges.mockResolvedValue(fullyAuthorizedCheckPrivilegesResponse); + + const result = await securityExtension.performAuthorization({ types, spaces, actions }); + + expect(result).toEqual({ + status: 'fully_authorized', + typeMap: new Map() + .set('a', { + foo: { isGloballyAuthorized: true, authorizedSpaces: [] }, + bar: { isGloballyAuthorized: true, authorizedSpaces: [] }, + // Technically, 'login:' is not a saved object action, it is a Kibana privilege -- however, we include it in the `typeMap` results + // for ease of use with the `redactNamespaces` function. The user is never actually authorized to "login" for a given object type, + // they are authorized to log in on a per-space basis, and this is applied to each object type in the typeMap result accordingly. + ['login:']: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }) + .set('b', { + foo: { authorizedSpaces: ['x', 'y'] }, + bar: { authorizedSpaces: ['x', 'y'] }, + ['login:']: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }) + .set('c', { + foo: { authorizedSpaces: ['x', 'y'] }, + bar: { authorizedSpaces: ['x', 'y'] }, + ['login:']: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }), + }); + }); + + test('partially authorized', async () => { + const { securityExtension, checkPrivileges } = setup(); + checkPrivileges.mockResolvedValue({ + hasAllRequested: false, + privileges: { + kibana: [ + // For type 'a', the user is authorized to use 'foo' action but not 'bar' action (all spaces) + // For type 'b', the user is authorized to use 'foo' action but not 'bar' action (both spaces) + // For type 'c', the user is authorized to use both actions in space 'x' but not space 'y' + { privilege: 'mock-saved_object:a/foo', authorized: true }, + { privilege: 'mock-saved_object:a/bar', authorized: false }, + { privilege: 'mock-saved_object:a/bar', authorized: true }, // fail-secure check + { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:b/bar', authorized: false }, + { resource: 'x', privilege: 'mock-saved_object:c/foo', authorized: true }, + { privilege: 'mock-saved_object:c/foo', authorized: false }, // inverse fail-secure check + { resource: 'x', privilege: 'mock-saved_object:c/bar', authorized: true }, + { resource: 'x', privilege: 'login:', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:b/bar', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:c/foo', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: false }, + { privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check + { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check + { resource: 'y', privilege: 'login:', authorized: true }, + // The fail-secure checks are a contrived scenario, as we *shouldn't* get both an unauthorized and authorized result for a given resource... + // However, in case we do, we should fail-secure (authorized + unauthorized = unauthorized) + ], + }, + } as CheckPrivilegesResponse); + + const result = await securityExtension.performAuthorization({ types, spaces, actions }); + expect(result).toEqual({ + status: 'partially_authorized', + typeMap: new Map() + .set('a', { + foo: { isGloballyAuthorized: true, authorizedSpaces: [] }, + ['login:']: { authorizedSpaces: ['x', 'y'] }, + }) + .set('b', { + foo: { authorizedSpaces: ['x', 'y'] }, + ['login:']: { authorizedSpaces: ['x', 'y'] }, + }) + .set('c', { + foo: { authorizedSpaces: ['x'] }, + bar: { authorizedSpaces: ['x'] }, + ['login:']: { authorizedSpaces: ['x', 'y'] }, + }), + }); + }); + + test('unauthorized', async () => { + const { securityExtension, checkPrivileges } = setup(); + checkPrivileges.mockResolvedValue({ + hasAllRequested: false, + privileges: { + kibana: [ + { privilege: 'mock-saved_object:a/foo', authorized: false }, + { privilege: 'mock-saved_object:a/bar', authorized: false }, + { privilege: 'mock-saved_object:a/bar', authorized: true }, // fail-secure check + { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: false }, + { resource: 'x', privilege: 'mock-saved_object:b/bar', authorized: false }, + { resource: 'x', privilege: 'mock-saved_object:c/foo', authorized: false }, + { resource: 'x', privilege: 'mock-saved_object:c/bar', authorized: false }, + { resource: 'x', privilege: 'login:', authorized: false }, + { resource: 'x', privilege: 'login:', authorized: true }, // fail-secure check + { resource: 'y', privilege: 'mock-saved_object:a/foo', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:a/bar', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:b/bar', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:c/foo', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: false }, + { privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check + { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check + { resource: 'y', privilege: 'login:', authorized: true }, // should *not* result in a 'partially_authorized' status + // The fail-secure checks are a contrived scenario, as we *shouldn't* get both an unauthorized and authorized result for a given resource... + // However, in case we do, we should fail-secure (authorized + unauthorized = unauthorized) + ], + }, + } as CheckPrivilegesResponse); + + const result = await securityExtension.performAuthorization({ types, spaces, actions }); + expect(result).toEqual({ + // The user is authorized to log into space Y, but they are not authorized to take any actions on any of the requested object types. + // Therefore, the status is 'unauthorized'. + status: 'unauthorized', + typeMap: new Map() + .set('a', { ['login:']: { authorizedSpaces: ['y'] } }) + .set('b', { ['login:']: { authorizedSpaces: ['y'] } }) + .set('c', { ['login:']: { authorizedSpaces: ['y'] } }), + }); + }); + + test('conflicting privilege failsafe', async () => { + const conflictingPrivilegesResponse = { + hasAllRequested: true, + privileges: { + kibana: [ + // redundant conflicting privileges for space X, type B, action Foo + { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: true }, + ], + }, + } as CheckPrivilegesResponse; + + const { securityExtension, checkPrivileges } = setup(); + checkPrivileges.mockResolvedValue(conflictingPrivilegesResponse); + + const result = await securityExtension.performAuthorization({ types, spaces, actions }); + expect(result).toEqual({ + status: 'fully_authorized', + typeMap: new Map().set('b', { + foo: { authorizedSpaces: ['y'] }, // should NOT be authorized for conflicted privilege + }), + }); + }); + }); + + describe('with enforce', () => { + // These arguments are used for all unit tests below + const types = new Set(['a', 'b', 'c']); + const spaces = new Set(['x', 'y']); + const actions = new Set(['foo']); + + const fullyAuthorizedCheckPrivilegesResponse = { + hasAllRequested: true, + privileges: { + kibana: [ + { privilege: 'mock-saved_object:a/foo', authorized: true }, + { privilege: 'login:', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:c/foo', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:c/foo', authorized: true }, + ], + }, + } as CheckPrivilegesResponse; + + const partiallyAuthorizedCheckPrivilegesResponse = { + hasAllRequested: false, + privileges: { + kibana: [ + { privilege: 'mock-saved_object:a/foo', authorized: true }, + { privilege: 'login:', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:c/foo', authorized: true }, + ], + }, + } as CheckPrivilegesResponse; + + const unauthorizedCheckPrivilegesResponse = { + hasAllRequested: false, + privileges: { + kibana: [ + { privilege: 'login:', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:a/foo', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'z', privilege: 'mock-saved_object:c/foo', authorized: true }, + ], + }, + } as CheckPrivilegesResponse; + + test('fully authorized', async () => { + const { securityExtension, checkPrivileges } = setup(); + checkPrivileges.mockResolvedValue(fullyAuthorizedCheckPrivilegesResponse); + + await expect(() => + securityExtension.performAuthorization({ + actions, + types, + spaces, + enforceMap: new Map([ + ['a', new Set(['x', 'y', 'z'])], + ['b', new Set(['x', 'y'])], + ['c', new Set(['y'])], + ]), + }) + ).not.toThrowError(); + }); + + test('partially authorized', async () => { + const { securityExtension, checkPrivileges } = setup(); + checkPrivileges.mockResolvedValue(partiallyAuthorizedCheckPrivilegesResponse); + + await expect(() => + securityExtension.performAuthorization({ + actions, + types, + spaces, + enforceMap: new Map([ + ['a', new Set(['x', 'y', 'z'])], + ['b', new Set(['x', 'y'])], + ['c', new Set(['x', 'y'])], + ]), + }) + ).rejects.toThrowError('Unable to foo b,c'); + }); + + test('unauthorized', async () => { + const { securityExtension, checkPrivileges } = setup(); + checkPrivileges.mockResolvedValue(unauthorizedCheckPrivilegesResponse); + + await expect(() => + securityExtension.performAuthorization({ + actions, + types, + spaces, + enforceMap: new Map([ + ['a', new Set(['y', 'z'])], + ['b', new Set(['x', 'z'])], + ['c', new Set(['x', 'y'])], + ]), + }) + ).rejects.toThrowError('Unable to foo a,b,c'); + }); + }); +}); + describe('#addAuditEvent', () => { test(`adds an unknown audit event`, async () => { const { auditLogger, securityExtension } = setup(); diff --git a/x-pack/plugins/security/server/saved_objects/saved_objects_security_extension.ts b/x-pack/plugins/security/server/saved_objects/saved_objects_security_extension.ts index 07c720c0aa59b..6a85951231a59 100644 --- a/x-pack/plugins/security/server/saved_objects/saved_objects_security_extension.ts +++ b/x-pack/plugins/security/server/saved_objects/saved_objects_security_extension.ts @@ -15,6 +15,7 @@ import type { CheckAuthorizationResult, EnforceAuthorizationParams, ISavedObjectsSecurityExtension, + PerformAuthorizationParams, RedactNamespacesParams, } from '@kbn/core-saved-objects-server'; @@ -45,7 +46,7 @@ export class SavedObjectsSecurityExtension implements ISavedObjectsSecurityExten this.checkPrivilegesFunc = checkPrivileges; } - async checkAuthorization( + private async checkAuthorization( params: CheckAuthorizationParams ): Promise> { const { types, spaces, actions, options = { allowGlobalResource: false } } = params; @@ -158,6 +159,31 @@ export class SavedObjectsSecurityExtension implements ISavedObjectsSecurityExten auditCallback?.(); } + async performAuthorization( + params: PerformAuthorizationParams + ): Promise> { + const checkResult: CheckAuthorizationResult = await this.checkAuthorization({ + types: params.types, + spaces: params.spaces, + actions: params.actions, + options: { allowGlobalResource: params.options?.allowGlobalResource === true }, + }); + + const typesAndSpaces = params.enforceMap; + if (typesAndSpaces !== undefined && checkResult) { + params.actions.forEach((action) => { + this.enforceAuthorization({ + typesAndSpaces, + action, + typeMap: checkResult.typeMap, + auditCallback: params.auditCallback, + }); + }); + } + + return checkResult; + } + addAuditEvent(params: AddAuditEventParams): void { if (this.auditLogger.enabled) { const auditEvent = savedObjectEvent(params); diff --git a/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts b/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts index caa45118e2879..c571c96e87e71 100644 --- a/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts @@ -8,6 +8,7 @@ import { savedObjectsExtensionsMock } from '@kbn/core-saved-objects-api-server-mocks'; import type { ISavedObjectsSecurityExtension } from '@kbn/core-saved-objects-server'; import { AuditAction } from '@kbn/core-saved-objects-server'; +import { setMapsAreEqual, setsAreEqual } from '@kbn/core-saved-objects-utils-server'; import type { EcsEventOutcome, SavedObjectsFindResponse } from '@kbn/core/server'; import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { httpServerMock } from '@kbn/core/server/mocks'; @@ -732,15 +733,34 @@ describe('SecureSpacesClientWrapper', () => { function expectAuthorizationCheck( securityExtension: jest.Mocked, - targetTypes: string[], - targetSpaces: string[] + aliases: Array<{ targetSpace: string; targetType: string }> ) { - expect(securityExtension!.checkAuthorization).toHaveBeenCalledTimes(1); - expect(securityExtension!.checkAuthorization).toHaveBeenCalledWith({ - types: new Set(targetTypes), // unique types of the alias targets - spaces: new Set(targetSpaces), // unique spaces of the alias targets - actions: new Set(['bulk_update']), - }); + expect(securityExtension.performAuthorization).toHaveBeenCalledTimes(1); + + const targetTypes = aliases.map((alias) => alias.targetType); + const targetSpaces = aliases.map((alias) => alias.targetSpace); + + const expectedActions = new Set(['bulk_update']); + const expectedSpaces = new Set(targetSpaces); + const expectedTypes = new Set(targetTypes); + const expectedEnforceMap = new Map>(); + aliases.forEach((alias) => { + expectedEnforceMap.set(alias.targetType, new Set([alias.targetSpace])); + }); + + const { + actions: actualActions, + spaces: actualSpaces, + types: actualTypes, + enforceMap: actualEnforceMap, + options: actualOptions, + } = securityExtension.performAuthorization.mock.calls[0][0]; + + expect(setsAreEqual(expectedActions, actualActions)).toBeTruthy(); + expect(setsAreEqual(expectedSpaces, actualSpaces)).toBeTruthy(); + expect(setsAreEqual(expectedTypes, actualTypes)).toBeTruthy(); + expect(setMapsAreEqual(expectedEnforceMap, actualEnforceMap)).toBeTruthy(); + expect(actualOptions).toBeUndefined(); } describe('when security is not enabled', () => { @@ -764,12 +784,7 @@ describe('SecureSpacesClientWrapper', () => { const { wrapper, baseClient, forbiddenError, securityExtension } = setup({ securityEnabled, }); - securityExtension!.checkAuthorization.mockResolvedValue({ - // These values don't actually matter, the call to enforceAuthorization matters - status: 'unauthorized', - typeMap: new Map(), - }); - securityExtension!.enforceAuthorization.mockImplementation(() => { + securityExtension!.performAuthorization.mockImplementation(() => { throw new Error('Oh no!'); }); const aliases = [alias1, alias2]; @@ -777,14 +792,14 @@ describe('SecureSpacesClientWrapper', () => { forbiddenError ); - expectAuthorizationCheck(securityExtension!, ['type-1', 'type-2'], ['space-1', 'space-2']); + expectAuthorizationCheck(securityExtension!, aliases); expectAuditEvents(securityExtension!, aliases, { error: true }); expect(baseClient.disableLegacyUrlAliases).not.toHaveBeenCalled(); }); it('updates the legacy URL aliases when authorized', async () => { const { wrapper, baseClient, securityExtension } = setup({ securityEnabled }); - securityExtension!.checkAuthorization.mockResolvedValue({ + securityExtension!.performAuthorization.mockResolvedValue({ // These values don't actually matter, the call to enforceAuthorization matters status: 'fully_authorized', typeMap: new Map(), @@ -793,7 +808,7 @@ describe('SecureSpacesClientWrapper', () => { const aliases = [alias1, alias2]; await wrapper.disableLegacyUrlAliases(aliases); - expectAuthorizationCheck(securityExtension!, ['type-1', 'type-2'], ['space-1', 'space-2']); + expectAuthorizationCheck(securityExtension!, aliases); expectAuditEvents(securityExtension!, aliases, { error: false }); expect(baseClient.disableLegacyUrlAliases).toHaveBeenCalledTimes(1); expect(baseClient.disableLegacyUrlAliases).toHaveBeenCalledWith(aliases); diff --git a/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.ts b/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.ts index 7e8adf5cb8ec9..ae89dc5991ee7 100644 --- a/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.ts +++ b/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.ts @@ -322,23 +322,20 @@ export class SecureSpacesClientWrapper implements ISpacesClient { [new Set(), new Map>()] ); - const { typeMap } = await this.securityExtension.checkAuthorization({ - types: new Set(typesAndSpaces.keys()), - spaces: uniqueSpaces, - actions: new Set(['bulk_update']), - }); let error: Error | undefined; try { - await this.securityExtension.enforceAuthorization({ - typesAndSpaces, - action: 'bulk_update', - typeMap, + await this.securityExtension.performAuthorization({ + actions: new Set(['bulk_update']), + types: new Set(typesAndSpaces.keys()), + spaces: uniqueSpaces, + enforceMap: typesAndSpaces, }); } catch (err) { error = this.errors.decorateForbiddenError( new Error(`Unable to disable aliases: ${err.message}`) ); } + for (const alias of aliases) { const id = getAliasId(alias); this.securityExtension.addAuditEvent({ From f9ae25f67cef4edfd44831f1e4928ea7012522b3 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 19 Dec 2022 15:21:00 -0500 Subject: [PATCH 28/55] [Fleet] Allow to preconfigure proxy_id for output and fleet server host (#147716) --- .../preconfiguration/fleet_server_host.ts | 20 ++++++++++--------- .../services/preconfiguration/outputs.ts | 17 ++++------------ .../server/services/preconfiguration/utils.ts | 19 ++++++++++++++++++ .../server/types/models/preconfiguration.ts | 1 + 4 files changed, 35 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/fleet/server/services/preconfiguration/utils.ts diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/fleet_server_host.ts b/x-pack/plugins/fleet/server/services/preconfiguration/fleet_server_host.ts index 15e2bf5d80a7b..3dc25332a848b 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration/fleet_server_host.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration/fleet_server_host.ts @@ -6,7 +6,6 @@ */ import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; -import { isEqual } from 'lodash'; import { decodeCloudId, normalizeHostsForAgents } from '../../../common/services'; import type { FleetConfigType } from '../../config'; @@ -24,6 +23,8 @@ import { } from '../fleet_server_host'; import { agentPolicyService } from '../agent_policy'; +import { isDifferent } from './utils'; + export function getCloudFleetServersHosts() { const cloudSetup = appContextService.getCloud(); if (cloudSetup && cloudSetup.isCloudEnabled && cloudSetup.cloudId && cloudSetup.deploymentId) { @@ -101,14 +102,15 @@ export async function createOrUpdatePreconfiguredFleetServerHosts( const isCreate = !existingHost; const isUpdateWithNewData = - existingHost && - (!existingHost.is_preconfigured || - existingHost.is_default !== preconfiguredFleetServerHost.is_default || - existingHost.name !== preconfiguredFleetServerHost.name || - !isEqual( - existingHost.host_urls.map(normalizeHostsForAgents), - preconfiguredFleetServerHost.host_urls.map(normalizeHostsForAgents) - )); + (existingHost && + (!existingHost.is_preconfigured || + existingHost.is_default !== preconfiguredFleetServerHost.is_default || + existingHost.name !== preconfiguredFleetServerHost.name || + isDifferent( + existingHost.host_urls.map(normalizeHostsForAgents), + preconfiguredFleetServerHost.host_urls.map(normalizeHostsForAgents) + ))) || + isDifferent(existingHost?.proxy_id, preconfiguredFleetServerHost.proxy_id); if (isCreate) { await createFleetServerHost( diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts index d7f43ce181a42..d51058fcb58e2 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts @@ -15,9 +15,10 @@ import type { FleetConfigType } from '../../config'; import { DEFAULT_OUTPUT_ID, DEFAULT_OUTPUT } from '../../constants'; import { outputService } from '../output'; import { agentPolicyService } from '../agent_policy'; - import { appContextService } from '../app_context'; +import { isDifferent } from './utils'; + export function getPreconfiguredOutputFromConfig(config?: FleetConfigType) { const { outputs: outputsOrUndefined } = config; @@ -151,17 +152,6 @@ export async function cleanPreconfiguredOutputs( } } -function isDifferent(val1: any, val2: any) { - if ( - (val1 === null || typeof val1 === 'undefined') && - (val2 === null || typeof val2 === 'undefined') - ) { - return false; - } - - return !isEqual(val1, val2); -} - function isPreconfiguredOutputDifferentFromCurrent( existingOutput: Output, preconfiguredOutput: Partial @@ -187,6 +177,7 @@ function isPreconfiguredOutputDifferentFromCurrent( existingOutput.ca_trusted_fingerprint, preconfiguredOutput.ca_trusted_fingerprint ) || - isDifferent(existingOutput.config_yaml, preconfiguredOutput.config_yaml) + isDifferent(existingOutput.config_yaml, preconfiguredOutput.config_yaml) || + isDifferent(existingOutput.proxy_id, preconfiguredOutput.proxy_id) ); } diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/utils.ts b/x-pack/plugins/fleet/server/services/preconfiguration/utils.ts new file mode 100644 index 0000000000000..442f80c9e2268 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/preconfiguration/utils.ts @@ -0,0 +1,19 @@ +/* + * 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 { isEqual } from 'lodash'; + +export function isDifferent(val1: any, val2: any) { + if ( + (val1 === null || typeof val1 === 'undefined') && + (val2 === null || typeof val2 === 'undefined') + ) { + return false; + } + + return !isEqual(val1, val2); +} diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts index 13ba525ee420b..e6b6a40ddb2b3 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts @@ -91,6 +91,7 @@ export const PreconfiguredFleetServerHostsSchema = schema.arrayOf( name: schema.string(), is_default: schema.boolean({ defaultValue: false }), host_urls: schema.arrayOf(schema.string(), { minSize: 1 }), + proxy_id: schema.nullable(schema.string()), }), { defaultValue: [] } ); From 2fd840a570b6031f076d8d5755b9d22591bd013f Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 19 Dec 2022 13:23:08 -0700 Subject: [PATCH 29/55] [generate/pkg] Fix BUILD.bazel template --- packages/kbn-generate/templates/package/BUILD.bazel.ejs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kbn-generate/templates/package/BUILD.bazel.ejs b/packages/kbn-generate/templates/package/BUILD.bazel.ejs index 92a407eea682c..4046385998554 100644 --- a/packages/kbn-generate/templates/package/BUILD.bazel.ejs +++ b/packages/kbn-generate/templates/package/BUILD.bazel.ejs @@ -131,8 +131,8 @@ filegroup( visibility = ["//visibility:public"], ) -filegroup( +pkg_npm( name = "build_types", - srcs = [":npm_module_types"], + deps = [":npm_module_types"], visibility = ["//visibility:public"], ) From d8bada0d12ad371588d72acda64e744a0a1dfd5f Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Mon, 19 Dec 2022 13:07:45 -0800 Subject: [PATCH 30/55] [Canvas] Adds functional test for esdocs datasource (#146942) ## Summary Test for fix #14620. This adds a functional test that creates a new data table element and changes the datasource to `esdocs` via the sidebar. I'm testing with a `logstash-*` data view with a missing `name` attribute to make sure Canvas doesn't error out when editing the esdocs settings. [Flaky test runner x 100](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1650) ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../datasource/datasource_component.js | 10 ++- .../datasource/datasource_selector.js | 1 + .../es_data_view_select.component.tsx | 7 +- .../element_settings.component.tsx | 4 + .../element_menu/element_menu.component.tsx | 2 + .../test/functional/apps/canvas/datasource.ts | 85 +++++++++++++++++++ x-pack/test/functional/apps/canvas/index.ts | 1 + .../functional/page_objects/canvas_page.ts | 34 ++++++++ 8 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/functional/apps/canvas/datasource.ts diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_component.js b/x-pack/plugins/canvas/public/components/datasource/datasource_component.js index 1a357b6722c71..4b64149d2a8f6 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource_component.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource_component.js @@ -172,6 +172,7 @@ export class DatasourceComponent extends PureComponent { className="canvasDataSource__triggerButton" flush="left" size="s" + data-test-subj="canvasChangeDatasourceButton" > {stateDatasource.displayName} @@ -188,7 +189,14 @@ export class DatasourceComponent extends PureComponent { - + {strings.getSaveButtonLabel()} diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_selector.js b/x-pack/plugins/canvas/public/components/datasource/datasource_selector.js index 6cfa005f92735..1f27c70d16f63 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource_selector.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource_selector.js @@ -25,6 +25,7 @@ export const DatasourceSelector = ({ onSelect, datasources, current }) => ( isSelected: d.name === current ? true : false, onClick: () => onSelect(d.name), }} + data-test-subj={`canvasDatasourceCard__${d.name}`} /> ))}
diff --git a/x-pack/plugins/canvas/public/components/es_data_view_select/es_data_view_select.component.tsx b/x-pack/plugins/canvas/public/components/es_data_view_select/es_data_view_select.component.tsx index 182c460748671..4539b1f7274fc 100644 --- a/x-pack/plugins/canvas/public/components/es_data_view_select/es_data_view_select.component.tsx +++ b/x-pack/plugins/canvas/public/components/es_data_view_select/es_data_view_select.component.tsx @@ -36,7 +36,11 @@ export const ESDataViewSelect: React.FunctionComponent = const selectedOption = selectedDataView ? { value: selectedDataView.title, label: selectedDataView.name || selectedDataView.title } : { value, label: value }; - const options = dataViews.map(({ name, title }) => ({ value: title, label: name || title })); + const options = dataViews.map(({ name, title }) => ({ + value: title, + label: name || title, + 'data-test-subj': `canvasDataViewSelect__${name || title}`, + })); return ( = isClearable={false} onCreateOption={(input) => onChange(input || defaultOption.value)} compressed + data-test-subj="canvasDataViewSelect" /> ); }; diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx index 6f1fabf26fecc..5d51d8a56dcb8 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx @@ -55,6 +55,7 @@ export const ElementSettings: FunctionComponent = ({ element }) => {
), + 'data-test-subj': 'canvasSidebarFiltersTab', }; return [ @@ -68,6 +69,7 @@ export const ElementSettings: FunctionComponent = ({ element }) => {
), + 'data-test-subj': 'canvasSidebarDisplayTab', }, { id: 'data', @@ -77,6 +79,7 @@ export const ElementSettings: FunctionComponent = ({ element }) => { ), + 'data-test-subj': 'canvasSidebarDataTab', }, ...(filtersTab ? [filtersTab] : []), ]; @@ -93,6 +96,7 @@ export const ElementSettings: FunctionComponent = ({ element }) => { key={tab.id} onClick={() => onSelectedTabChanged(tab.id)} isSelected={tab.id === selectedTab} + data-test-subj={tab['data-test-subj']} > {tab.name} diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx index d45b723dd53cd..784c040ee6174 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx @@ -146,6 +146,7 @@ export const ElementMenu: FunctionComponent = ({ elements, addElement }) addElement(element); closePopover(); }, + 'data-test-subj': `canvasAddElementMenu__${element.name}`, }); const elementListToMenuItems = (elementList: ElementSpec[]) => { @@ -161,6 +162,7 @@ export const ElementMenu: FunctionComponent = ({ elements, addElement }) title: name, items: elementList.map(elementToMenuItem), }, + 'data-test-subj': `canvasAddElementMenu__${name}`, }; } diff --git a/x-pack/test/functional/apps/canvas/datasource.ts b/x-pack/test/functional/apps/canvas/datasource.ts new file mode 100644 index 0000000000000..c1cf907bc5342 --- /dev/null +++ b/x-pack/test/functional/apps/canvas/datasource.ts @@ -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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function canvasExpressionTest({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['canvas', 'common']); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const monacoEditor = getService('monacoEditor'); + + describe('datasource', function () { + // there is an issue with FF not properly clicking on workpad elements + this.tags('skipFirefox'); + + before(async () => { + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded( + 'test/functional/fixtures/es_archiver/kibana_sample_data_flights' + ); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/legacy.json'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/kibana_sample_data_flights_index_pattern.json' + ); + + await kibanaServer.uiSettings.update({ + defaultIndex: 'kibana_sample_data_flights', + }); + + // create new test workpad + await PageObjects.common.navigateToApp('canvas'); + await PageObjects.canvas.createNewWorkpad(); + }); + + after(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.unload('test/functional/fixtures/es_archiver/kibana_sample_data_flights'); + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/legacy.json'); + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/kibana_sample_data_flights_index_pattern.json' + ); + }); + + describe('esdocs', function () { + it('sidebar shows to esdocs datasource settings', async () => { + await PageObjects.canvas.createNewDatatableElement(); + + // find the first workpad element (a markdown element) and click it to select it + await testSubjects.click('canvasWorkpadPage > canvasWorkpadPageElementContent', 20000); + + // open Data tab + await PageObjects.canvas.openDatasourceTab(); + + // change datasource to esdocs + await PageObjects.canvas.changeDatasourceTo('esdocs'); + + // click data view select + await testSubjects.click('canvasDataViewSelect'); + + // check that data view options list is displayed + expect(await testSubjects.exists('canvasDataViewSelect-optionsList')); + + // check that the logstash-* data view without name attribute is available + expect(await testSubjects.exists('canvasDataViewSelect__logstash-*')); + }); + + it('updates expression to use esdocs', async () => { + await testSubjects.click('canvasDataViewSelect__logstash-*'); + + await PageObjects.canvas.saveDatasourceChanges(); + + await PageObjects.canvas.openExpressionEditor(); + await monacoEditor.waitCodeEditorReady('canvasExpressionInput'); + expect(await monacoEditor.getCodeEditorValue()).contain('esdocs index="logstash-*"'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/canvas/index.ts b/x-pack/test/functional/apps/canvas/index.ts index 7f3487314d4d8..7b4192fbd3924 100644 --- a/x-pack/test/functional/apps/canvas/index.ts +++ b/x-pack/test/functional/apps/canvas/index.ts @@ -43,6 +43,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./expression')); loadTestFile(require.resolve('./filters')); loadTestFile(require.resolve('./custom_elements')); + loadTestFile(require.resolve('./datasource')); loadTestFile(require.resolve('./feature_controls/canvas_security')); loadTestFile(require.resolve('./feature_controls/canvas_spaces')); loadTestFile(require.resolve('./embeddables/lens')); diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts index a51b878b6af30..5cc801be9fbeb 100644 --- a/x-pack/test/functional/page_objects/canvas_page.ts +++ b/x-pack/test/functional/page_objects/canvas_page.ts @@ -76,12 +76,30 @@ export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderCo expect(disabledAttr).to.be('true'); }, + async openAddElementMenu() { + log.debug('openAddElementsMenu'); + await testSubjects.click('add-element-button'); + }, + + async openAddChartMenu() { + log.debug('openAddChartMenu'); + await this.openAddElementMenu(); + await testSubjects.click('canvasAddElementMenu__Chart'); + }, + + async createNewDatatableElement() { + log.debug('createNewDatatableElement'); + await this.openAddChartMenu(); + await testSubjects.click('canvasAddElementMenu__table'); + }, + async openSavedElementsModal() { await testSubjects.click('add-element-button'); await testSubjects.click('saved-elements-menu-option'); await PageObjects.common.sleep(1000); // give time for modal animation to complete }, + async closeSavedElementsModal() { await testSubjects.click('saved-elements-modal-close-button'); }, @@ -157,5 +175,21 @@ export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderCo await testSubjects.click('canvasWorkpadEditMenuButton'); await testSubjects.click('canvasEditMenuDeleteButton'); }, + + async openDatasourceTab() { + log.debug('CanvasPage.openDataTab'); + await testSubjects.click('canvasSidebarDataTab'); + }, + + async changeDatasourceTo(datasourceName: string) { + log.debug('CanvasPage.changeDatasourceTo'); + await testSubjects.click('canvasChangeDatasourceButton'); + await testSubjects.click(`canvasDatasourceCard__${datasourceName}`); + }, + + async saveDatasourceChanges() { + log.debug('CanvasPage.saveDatasourceChanges'); + await testSubjects.click('canvasSaveDatasourceButton'); + }, }; } From 6bdfc483967c6920c5da4b539400c0baef94df93 Mon Sep 17 00:00:00 2001 From: Bree Hall <40739624+breehall@users.noreply.github.com> Date: Mon, 19 Dec 2022 16:14:10 -0500 Subject: [PATCH 31/55] Bumping EUI to 71.0.0 (#147142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit eui@70.4.0 ⏩ eui@71.0.0 --- ## [`71.0.0`](https://github.com/elastic/eui/tree/v71.0.0) - Implemented new `EuiRange` and `EuiDualRange` designs where the `levels` are now on top of the tracks ([#6092](https://github.com/elastic/eui/pull/6092)) - Added `discuss` and `dotInCircle` glyphs to `EuiIcon` ([#6434](https://github.com/elastic/eui/pull/6434)) - Added `article` glyph to `EuiIcon` ([#6437](https://github.com/elastic/eui/pull/6437)) - Changed the `EuiProvider` usage warnings to not rely on development mode. ([#6451](https://github.com/elastic/eui/pull/6451)) **Breaking changes** - `EuiDualRange` now explicitly requires both `min` and `max` via props types, to match `EuiRange` ([#6092](https://github.com/elastic/eui/pull/6092)) - `EuiRange` and `EuiDualRange`'s `compressed` size no longer impacts track or level sizes, but continues to compress tick and input sizes. ([#6092](https://github.com/elastic/eui/pull/6092)) - Removed all variables for the following components from EUI's theme JSON files: ([#6443](https://github.com/elastic/eui/pull/6443)) - `euiCollapsibleNav*` - `euiColorPicker*` - `euiContextMenu*` - `euiControlBar*` - `euiDataGrid* `(except for z-indices and cell padding sizes) - `euiDatePicker*` - `euiSuperDatePicker*` - `euiDragAndDrop*` - `euiEuiEmptyPrompt*` - `euiFilePicker*` - `euiRange*` - `euiHeaderLinks*` - `euiKeyPad*` - `euiMarkdownEditor*` - `euiResizable*` - `euiSelectable*` - `euiSideNav*` - `euiStep*` - `euiSuggest*` - `euiTable*` (except for color variables) - `euiTooltip*` - `euiButtonFontWeight`, `euiButtonDefaultTransparency`, and `euiButtonMinWidth` - If you were importing any of the above removed JSON variables, we strongly recommend using generic color or sizing variables from `useEuiTheme()` instead. ([#6443](https://github.com/elastic/eui/pull/6443)) **CSS-in-JS conversions** - Converted `EuiRange` and `EuiDualRange` to Emotion; Removed `$euiRangeThumbRadius` ([#6092](https://github.com/elastic/eui/pull/6092)) - Added a new `logicalStyles` utility that automatically converts all non-logical properties in a `style` object to their corresponding logical properties ([#6426](https://github.com/elastic/eui/pull/6426)) - Added a new `logicalShorthandCSS` utility that automatically converts `margin`, `padding`, and other 4-sided shorthands to their corresponding logical properties ([#6429](https://github.com/elastic/eui/pull/6429)) - Added a new `logicalBorderRadiusCSS` utility that automatically converts `border-radius` to corresponding logical properties ([#6429](https://github.com/elastic/eui/pull/6429)) Co-authored-by: Constance Chen Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli --- package.json | 2 +- .../header/__snapshots__/header.test.tsx.snap | 1 + .../header_breadcrumbs.test.tsx.snap | 8 ++++ .../__snapshots__/list_header.test.tsx.snap | 8 ++++ src/dev/license_checker/config.ts | 2 +- .../components/range_slider_control.tsx | 13 +++--- .../components/range_slider_popover.tsx | 13 +++--- .../time_slider/components/time_slider.tsx | 6 +-- .../time_slider_popover_content.tsx | 18 ++++++-- .../time_slider/time_slider_reducers.ts | 2 +- .../controls/public/time_slider/time_utils.ts | 2 +- .../controls/public/time_slider/types.ts | 2 +- .../forms/components/fields/range_field.tsx | 10 ++--- .../__snapshots__/range_control.test.tsx.snap | 2 + .../public/components/vis/range_control.tsx | 2 +- .../validated_range/validated_dual_range.tsx | 7 +-- .../public/components/controls/precision.tsx | 4 +- .../controls/radius_ratio_option.tsx | 7 ++- .../__snapshots__/point_options.test.tsx.snap | 8 +--- .../partition_labels/extended_template.tsx | 14 +++++- .../group_source_prioritization.test.tsx | 2 +- .../group_source_prioritization.tsx | 13 +++--- .../pages/metrics/settings/input_fields.tsx | 10 ++++- .../resolution_editor.test.tsx.snap | 36 ++------------- .../es_geo_grid_source/resolution_editor.tsx | 6 +-- .../ordinal_data_mapping_popover.tsx | 5 ++- .../components/size/size_range_selector.tsx | 2 +- .../validated_range.test.js.snap | 30 ++----------- .../severity_control/severity_control.tsx | 11 +---- .../configuration_step_form.tsx | 4 +- .../form/shards/shards_percentage_field.tsx | 5 ++- .../__snapshots__/prompt_page.test.tsx.snap | 4 +- .../unauthenticated_page.test.tsx.snap | 2 +- .../reset_session_page.test.tsx.snap | 2 +- .../anomaly_threshold_slider/index.test.tsx | 2 +- .../rules/anomaly_threshold_slider/index.tsx | 11 ++--- .../rules/risk_score_mapping/index.tsx | 5 ++- .../public/resolver/view/graph_controls.tsx | 4 +- .../public/components/tty_player/styles.ts | 6 --- .../components/tty_player_controls/index.tsx | 9 ++-- .../tty_player_controls_markers/index.tsx | 6 +-- .../monitor_list_table/monitor_locations.tsx | 4 +- .../__snapshots__/donut_chart.test.tsx.snap | 44 +++++++++---------- .../outlier_detection_creation.ts | 4 +- yarn.lock | 8 ++-- 45 files changed, 166 insertions(+), 200 deletions(-) diff --git a/package.json b/package.json index ae72182a5dc1c..851824b97b505 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.5.0-canary.1", "@elastic/ems-client": "8.3.3", - "@elastic/eui": "70.4.0", + "@elastic/eui": "71.0.0", "@elastic/filesaver": "1.1.2", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.1", diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/header/__snapshots__/header.test.tsx.snap b/packages/core/chrome/core-chrome-browser-internal/src/ui/header/__snapshots__/header.test.tsx.snap index aa77a58ad7202..6b86cffc5ca20 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/ui/header/__snapshots__/header.test.tsx.snap +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/header/__snapshots__/header.test.tsx.snap @@ -156,6 +156,7 @@ exports[`Header renders 1`] = ` >
  • { - const rangeRef = useRef(null); + const rangeRef = useRef(null); const [isPopoverOpen, setIsPopoverOpen] = useState(false); // Controls Services Context @@ -143,13 +142,11 @@ export const RangeSliderControl: FC = () => { anchorPosition="downCenter" attachToAnchor={false} disableFocusTrap - onPanelResize={() => { - if (rangeRef?.current) { - rangeRef.current.onResize(); - } + onPanelResize={(width) => { + rangeRef.current?.onResize(width); }} > - + ); }; diff --git a/src/plugins/controls/public/range_slider/components/range_slider_popover.tsx b/src/plugins/controls/public/range_slider/components/range_slider_popover.tsx index fea4db4fc268d..bd2fee564e9cb 100644 --- a/src/plugins/controls/public/range_slider/components/range_slider_popover.tsx +++ b/src/plugins/controls/public/range_slider/components/range_slider_popover.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { FC, useEffect, useRef, useState } from 'react'; +import React, { FC, ComponentProps, Ref, useEffect, useState } from 'react'; import useMount from 'react-use/lib/useMount'; import { @@ -18,6 +18,7 @@ import { EuiToolTip, EuiButtonIcon, } from '@elastic/eui'; +import type { EuiDualRangeClass } from '@elastic/eui/src/components/form/range/dual_range'; import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public'; import { RangeValue } from '../../../common/range_slider/types'; @@ -26,9 +27,11 @@ import { rangeSliderReducers } from '../range_slider_reducers'; import { RangeSliderReduxState } from '../types'; import { RangeSliderStrings } from './range_slider_strings'; -export const RangeSliderPopover: FC = () => { +// Unfortunately, wrapping EuiDualRange in `withEuiTheme` has created this annoying/verbose typing +export type EuiDualRangeRef = EuiDualRangeClass & ComponentProps; + +export const RangeSliderPopover: FC<{ rangeRef?: Ref }> = ({ rangeRef }) => { const [fieldFormatter, setFieldFormatter] = useState(() => (toFormat: string) => toFormat); - const rangeRef = useRef(null); // Controls Services Context const { @@ -143,8 +146,8 @@ export const RangeSliderPopover: FC = () => { { const updatedLowerBound = typeof newLowerBound === 'number' ? String(newLowerBound) : value[0]; diff --git a/src/plugins/controls/public/time_slider/components/time_slider.tsx b/src/plugins/controls/public/time_slider/components/time_slider.tsx index bb8c698ee7c3b..3962d85bec1d9 100644 --- a/src/plugins/controls/public/time_slider/components/time_slider.tsx +++ b/src/plugins/controls/public/time_slider/components/time_slider.tsx @@ -7,12 +7,12 @@ */ import React, { FC, useRef } from 'react'; -import { EuiInputPopover, EuiDualRange } from '@elastic/eui'; +import { EuiInputPopover } from '@elastic/eui'; import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public'; import { timeSliderReducers } from '../time_slider_reducers'; import { TimeSliderReduxState } from '../types'; import { TimeSliderPopoverButton } from './time_slider_popover_button'; -import { TimeSliderPopoverContent } from './time_slider_popover_content'; +import { TimeSliderPopoverContent, EuiDualRangeRef } from './time_slider_popover_content'; import { FROM_INDEX, TO_INDEX } from '../time_utils'; import { getRoundedTimeRangeBounds } from '../time_slider_selectors'; @@ -46,7 +46,7 @@ export const TimeSlider: FC = (props: Props) => { return state.componentState.isOpen; }); - const rangeRef = useRef(null); + const rangeRef = useRef(null); const onPanelResize = (width?: number) => { rangeRef.current?.onResize(width); diff --git a/src/plugins/controls/public/time_slider/components/time_slider_popover_content.tsx b/src/plugins/controls/public/time_slider/components/time_slider_popover_content.tsx index 9ec21f7352817..3ce28761f713c 100644 --- a/src/plugins/controls/public/time_slider/components/time_slider_popover_content.tsx +++ b/src/plugins/controls/public/time_slider/components/time_slider_popover_content.tsx @@ -7,9 +7,19 @@ */ import { i18n } from '@kbn/i18n'; -import React, { Ref } from 'react'; -import { EuiButtonIcon, EuiDualRange, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import { EuiRangeTick } from '@elastic/eui/src/components/form/range/range_ticks'; +import React, { Ref, ComponentProps } from 'react'; +import { + EuiButtonIcon, + EuiDualRange, + EuiRangeTick, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, +} from '@elastic/eui'; +import type { EuiDualRangeClass } from '@elastic/eui/src/components/form/range/dual_range'; + +// Unfortunately, wrapping EuiDualRange in `withEuiTheme` has created a super annoying/verbose typing +export type EuiDualRangeRef = EuiDualRangeClass & ComponentProps; interface Props { value: [number, number]; @@ -19,7 +29,7 @@ interface Props { ticks: EuiRangeTick[]; timeRangeMin: number; timeRangeMax: number; - rangeRef?: Ref; + rangeRef?: Ref; } export function TimeSliderPopoverContent(props: Props) { diff --git a/src/plugins/controls/public/time_slider/time_slider_reducers.ts b/src/plugins/controls/public/time_slider/time_slider_reducers.ts index 160d4bf5b25ad..eb550d998f3af 100644 --- a/src/plugins/controls/public/time_slider/time_slider_reducers.ts +++ b/src/plugins/controls/public/time_slider/time_slider_reducers.ts @@ -8,7 +8,7 @@ import { PayloadAction } from '@reduxjs/toolkit'; import { WritableDraft } from 'immer/dist/types/types-external'; -import { EuiRangeTick } from '@elastic/eui/src/components/form/range/range_ticks'; +import { EuiRangeTick } from '@elastic/eui'; import { TimeSliderReduxState } from './types'; export const timeSliderReducers = { diff --git a/src/plugins/controls/public/time_slider/time_utils.ts b/src/plugins/controls/public/time_slider/time_utils.ts index bd978e9f7ec7e..89995bb4b0cf5 100644 --- a/src/plugins/controls/public/time_slider/time_utils.ts +++ b/src/plugins/controls/public/time_slider/time_utils.ts @@ -7,7 +7,7 @@ */ import moment from 'moment-timezone'; -import { EuiRangeTick } from '@elastic/eui/src/components/form/range/range_ticks'; +import { EuiRangeTick } from '@elastic/eui'; import { calcAutoIntervalNear } from '@kbn/data-plugin/common'; const MAX_TICKS = 20; // eui range has hard limit of 20 ticks and throws when exceeded diff --git a/src/plugins/controls/public/time_slider/types.ts b/src/plugins/controls/public/time_slider/types.ts index 56c78b0e45c2d..9e72b7dd50113 100644 --- a/src/plugins/controls/public/time_slider/types.ts +++ b/src/plugins/controls/public/time_slider/types.ts @@ -7,7 +7,7 @@ */ import { ReduxEmbeddableState } from '@kbn/presentation-util-plugin/public'; -import { EuiRangeTick } from '@elastic/eui/src/components/form/range/range_ticks'; +import { EuiRangeTick } from '@elastic/eui'; import { ControlOutput } from '../types'; import { TimeSliderControlEmbeddableInput } from '../../common/time_slider/types'; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx index c063e46a247b0..872e442943e0d 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx @@ -7,7 +7,7 @@ */ import React, { useCallback } from 'react'; -import { EuiFormRow, EuiRange } from '@elastic/eui'; +import { EuiFormRow, EuiRange, EuiRangeProps } from '@elastic/eui'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; @@ -22,11 +22,9 @@ export const RangeField = ({ field, euiFieldProps = {}, idAria, ...rest }: Props const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); const { onChange: onFieldChange } = field; - const onChange = useCallback( - (e: React.ChangeEvent | React.MouseEvent) => { - const event = { ...e, value: `${e.currentTarget.value}` } as unknown as React.ChangeEvent<{ - value: string; - }>; + const onChange: EuiRangeProps['onChange'] = useCallback( + (e) => { + const event = { ...e, value: `${e.currentTarget.value}` }; onFieldChange(event); }, [onFieldChange] diff --git a/src/plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap index c54397bf0e30c..8bd5c5bd46041 100644 --- a/src/plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap +++ b/src/plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap @@ -12,6 +12,8 @@ exports[`disabled 1`] = ` compressed={false} disabled={true} fullWidth={false} + max={100} + min={0} showInput={true} /> diff --git a/src/plugins/input_control_vis/public/components/vis/range_control.tsx b/src/plugins/input_control_vis/public/components/vis/range_control.tsx index dd003a74369df..602cc79dd8f5c 100644 --- a/src/plugins/input_control_vis/public/components/vis/range_control.tsx +++ b/src/plugins/input_control_vis/public/components/vis/range_control.tsx @@ -82,7 +82,7 @@ export class RangeControl extends PureComponent; + return ; } const decimalPlaces = _.get(this.props, 'control.options.decimalPlaces', 0); diff --git a/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx index 1b929be8af8ba..14dc13962fa8b 100644 --- a/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx +++ b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx @@ -8,9 +8,8 @@ import { i18n } from '@kbn/i18n'; import React, { Component, ReactNode } from 'react'; -import { EuiFormRow, EuiDualRange } from '@elastic/eui'; +import { EuiFormRow, EuiDualRange, EuiDualRangeProps } from '@elastic/eui'; import { EuiFormRowDisplayKeys } from '@elastic/eui/src/components/form/form_row/form_row'; -import { EuiDualRangeProps } from '@elastic/eui/src/components/form/range/dual_range'; import { isRangeValid } from './is_range_valid'; // Wrapper around EuiDualRange that ensures onChange callback is only called when range value @@ -19,14 +18,12 @@ import { isRangeValid } from './is_range_valid'; export type Value = EuiDualRangeProps['value']; export type ValueMember = EuiDualRangeProps['value'][0]; -interface Props extends Omit { +interface Props extends Omit { value?: Value; allowEmptyRange?: boolean; label?: string | ReactNode; formRowDisplay?: EuiFormRowDisplayKeys; onChange?: (val: [string, string]) => void; - min?: number; - max?: number; } interface State { diff --git a/src/plugins/vis_default_editor/public/components/controls/precision.tsx b/src/plugins/vis_default_editor/public/components/controls/precision.tsx index 2e9c2b252fa63..872c254cc9105 100644 --- a/src/plugins/vis_default_editor/public/components/controls/precision.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/precision.tsx @@ -31,9 +31,7 @@ function PrecisionParamEditor({ agg, value, setValue }: AggParamEditorProps | React.MouseEvent) => - setValue(Number(ev.currentTarget.value)) - } + onChange={(ev) => setValue(Number(ev.currentTarget.value))} data-test-subj={`visEditorMapPrecision${agg.id}`} showValue compressed diff --git a/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx b/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx index 408204b2a2dfb..b97a68f0c5984 100644 --- a/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/radius_ratio_option.tsx @@ -7,7 +7,7 @@ */ import React, { useCallback } from 'react'; -import { EuiFormRow, EuiIconTip, EuiRange, EuiSpacer } from '@elastic/eui'; +import { EuiFormRow, EuiIconTip, EuiRange, EuiRangeProps, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import useMount from 'react-use/lib/useMount'; @@ -40,9 +40,8 @@ function RadiusRatioOptionControl({ editorStateParams, setStateParamValue }: Agg } }); - const onChange = useCallback( - (e: React.ChangeEvent | React.MouseEvent) => - setStateParamValue(PARAM_NAME, parseFloat(e.currentTarget.value)), + const onChange: EuiRangeProps['onChange'] = useCallback( + (e) => setStateParamValue(PARAM_NAME, parseFloat(e.currentTarget.value)), [setStateParamValue] ); diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/point_options.test.tsx.snap b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/point_options.test.tsx.snap index fcd6f8d00a138..43b613a9c7f47 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/point_options.test.tsx.snap +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/point_options.test.tsx.snap @@ -23,21 +23,15 @@ exports[`PointOptions component should init with the default set of props 1`] = label="Dots size" labelType="label" > - diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/extended_template.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/extended_template.tsx index b5aa5d6184c99..af74acc622789 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/extended_template.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/partition_labels/extended_template.tsx @@ -5,7 +5,14 @@ * 2.0. */ -import React, { ChangeEvent, MouseEvent, FunctionComponent, useCallback, useEffect } from 'react'; +import React, { + ChangeEvent, + MouseEvent, + KeyboardEvent, + FunctionComponent, + useCallback, + useEffect, +} from 'react'; import PropTypes from 'prop-types'; import { EuiFormRow, @@ -80,7 +87,10 @@ export const ExtendedTemplate: FunctionComponent = ({ onValueChange, argV const onCommonFieldChange = useCallback( (field: Fields) => ( - event: ChangeEvent | MouseEvent + event: + | ChangeEvent + | KeyboardEvent + | MouseEvent ) => { onChangeField(field, event.currentTarget.value); }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx index 49dc7223e54e6..0a821deff90c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx @@ -73,7 +73,7 @@ describe('GroupSourcePrioritization', () => { const wrapper = shallow(); const slider = wrapper.find(EuiRange).first(); - slider.simulate('change', { target: { value: 2 } }); + slider.simulate('change', { currentTarget: { value: 2 } }); expect(updatePriority).toHaveBeenCalledWith('123', 2); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx index 615e7b22b343f..f72d4825aa040 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { ChangeEvent, MouseEvent } from 'react'; +import React from 'react'; import { useActions, useValues } from 'kea'; @@ -24,6 +24,7 @@ import { EuiTableRow, EuiTableRowCell, } from '@elastic/eui'; +import { _SingleRangeChangeEvent } from '@elastic/eui/src/components/form/range/types'; import { i18n } from '@kbn/i18n'; import { SAVE_BUTTON_LABEL } from '../../../../shared/constants'; @@ -91,10 +92,8 @@ export const GroupSourcePrioritization: React.FC = () => { {SAVE_BUTTON_LABEL} ); - const handleSliderChange = ( - id: string, - e: ChangeEvent | MouseEvent - ) => updatePriority(id, Number((e.target as HTMLInputElement).value)); + const handleSliderChange = (id: string, e: _SingleRangeChangeEvent) => + updatePriority(id, Number(e.currentTarget.value)); const hasSources = contentSources.length > 0; const zeroState = ( @@ -150,9 +149,7 @@ export const GroupSourcePrioritization: React.FC = () => { step={1} showInput value={activeSourcePriorities[id]} - onChange={(e: ChangeEvent | MouseEvent) => - handleSliderChange(id, e) - } + onChange={(e) => handleSliderChange(id, e)} /> diff --git a/x-pack/plugins/infra/public/pages/metrics/settings/input_fields.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/input_fields.tsx index e2380dedaee15..72e71143e2c84 100644 --- a/x-pack/plugins/infra/public/pages/metrics/settings/input_fields.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings/input_fields.tsx @@ -56,7 +56,10 @@ export interface InputRangeFieldProps< isInvalid: boolean; name: string; onChange?: ( - evt: React.ChangeEvent | React.MouseEvent, + evt: + | React.ChangeEvent + | React.KeyboardEvent + | React.MouseEvent, isValid: boolean ) => void; value: Value; @@ -81,7 +84,10 @@ export const createInputRangeFieldProps = < isInvalid: errors.length > 0, name, onChange: ( - evt: React.ChangeEvent | React.MouseEvent, + evt: + | React.ChangeEvent + | React.KeyboardEvent + | React.MouseEvent, isValid: boolean ) => onChange(+evt.currentTarget.value, isValid), value, diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/resolution_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/resolution_editor.test.tsx.snap index 79be012887af9..dfff4bdd6ca44 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/resolution_editor.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/resolution_editor.test.tsx.snap @@ -10,19 +10,12 @@ exports[`should render 3 tick slider when renderAs is HEX 1`] = ` label="Resolution" labelType="label" > - - - - { return resolution ? (resolution as GRID_RESOLUTION) : GRID_RESOLUTION.COARSE; } - _onResolutionChange = (event: ChangeEvent | MouseEvent) => { + _onResolutionChange: EuiRangeProps['onChange'] = (event) => { const resolution = this._sliderValueToResolution(parseInt(event.currentTarget.value, 10)); if (isMvt(this.props.renderAs, resolution)) { const hasUnsupportedMetrics = this.props.metrics.find(isUnsupportedVectorTileMetric); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/data_mapping/ordinal_data_mapping_popover.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/data_mapping/ordinal_data_mapping_popover.tsx index 3f3266d399c70..a614d707d0c83 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/data_mapping/ordinal_data_mapping_popover.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/data_mapping/ordinal_data_mapping_popover.tsx @@ -6,7 +6,7 @@ */ import _ from 'lodash'; -import React, { ChangeEvent, Fragment, MouseEvent } from 'react'; +import React, { Fragment } from 'react'; import { EuiFormRow, EuiHorizontalRule, @@ -18,6 +18,7 @@ import { EuiText, EuiToolTip, } from '@elastic/eui'; +import type { _SingleRangeChangeEvent } from '@elastic/eui/src/components/form/range/types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { DEFAULT_SIGMA } from '../../vector_style_defaults'; @@ -95,7 +96,7 @@ export function OrdinalDataMappingPopover(props: Props | MouseEvent) { + function onSigmaChange(event: _SingleRangeChangeEvent) { // @ts-expect-error props.onChange({ fieldMetaOptions: { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/size/size_range_selector.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/size/size_range_selector.tsx index 59bdfa3be5938..bc3b29a765398 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/size/size_range_selector.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/size/size_range_selector.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { ValidatedDualRange } from '@kbn/kibana-react-plugin/public'; -import { EuiDualRangeProps } from '@elastic/eui/src/components/form/range/dual_range'; +import { EuiDualRangeProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { MIN_SIZE, MAX_SIZE } from '../../vector_style_defaults'; diff --git a/x-pack/plugins/maps/public/components/__snapshots__/validated_range.test.js.snap b/x-pack/plugins/maps/public/components/__snapshots__/validated_range.test.js.snap index cee44b3e6b27c..dd9b6dcc3a61c 100644 --- a/x-pack/plugins/maps/public/components/__snapshots__/validated_range.test.js.snap +++ b/x-pack/plugins/maps/public/components/__snapshots__/validated_range.test.js.snap @@ -2,19 +2,10 @@ exports[`Should display error message when value is outside of range 1`] = `
    - @@ -33,37 +24,22 @@ exports[`Should display error message when value is outside of range 1`] = ` `; exports[`Should pass slider props to slider 1`] = ` - `; exports[`Should render slider 1`] = ` - `; diff --git a/x-pack/plugins/ml/public/application/components/severity_control/severity_control.tsx b/x-pack/plugins/ml/public/application/components/severity_control/severity_control.tsx index de3585ce9f63c..1ac6fc5ff46de 100644 --- a/x-pack/plugins/ml/public/application/components/severity_control/severity_control.tsx +++ b/x-pack/plugins/ml/public/application/components/severity_control/severity_control.tsx @@ -54,13 +54,6 @@ export const SeverityControl: FC = React.memo(({ value, o const resultValue = value ?? ANOMALY_THRESHOLD.LOW; - const onChangeCallback = ( - e: React.ChangeEvent | React.MouseEvent - ) => { - // @ts-ignore Property 'value' does not exist on type 'EventTarget' | (EventTarget & HTMLInputElement) - onChange(Number(e.target.value)); - }; - const ticks = new Array(5).fill(null).map((x, i) => { const v = i * 25; return { value: v, label: v }; @@ -76,7 +69,7 @@ export const SeverityControl: FC = React.memo(({ value, o compressed prepend={label} value={resultValue} - onChange={onChangeCallback} + onChange={(e) => onChange(Number(e.target.value))} min={ANOMALY_THRESHOLD.LOW} max={MAX_ANOMALY_SCORE} /> @@ -88,7 +81,7 @@ export const SeverityControl: FC = React.memo(({ value, o min={ANOMALY_THRESHOLD.LOW} max={MAX_ANOMALY_SCORE} value={resultValue} - onChange={onChangeCallback} + onChange={(e) => onChange(Number(e.currentTarget.value))} aria-label={i18n.translate('xpack.ml.severitySelector.formControlAriaLabel', { defaultMessage: 'Select severity threshold', })} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index 724aed7b08294..052c4e64272c9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -285,9 +285,7 @@ export const ConfigurationStepForm: FC = ({ const formStateUpdated = { ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemory } : {}), - ...(depVarIsRuntimeField || jobTypeChanged || depVarNotIncluded - ? { includes: formToUse.includes } - : {}), + ...(depVarIsRuntimeField || depVarNotIncluded ? { includes: formToUse.includes } : {}), requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, }; diff --git a/x-pack/plugins/osquery/public/packs/form/shards/shards_percentage_field.tsx b/x-pack/plugins/osquery/public/packs/form/shards/shards_percentage_field.tsx index 61a98219f3c38..4afb27570689e 100644 --- a/x-pack/plugins/osquery/public/packs/form/shards/shards_percentage_field.tsx +++ b/x-pack/plugins/osquery/public/packs/form/shards/shards_percentage_field.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useMemo } from 'react'; import { useController } from 'react-hook-form'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiRange } from '@elastic/eui'; +import type { EuiRangeProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { ShardsFormReturn } from './shards_form'; @@ -32,8 +33,8 @@ const ShardsPercentageFieldComponent = ({ defaultValue: 100, }); - const handleChange = useCallback( - (e: React.ChangeEvent | React.MouseEvent) => { + const handleChange: EuiRangeProps['onChange'] = useCallback( + (e) => { const numberValue = (e.target as { valueAsNumber: number }).valueAsNumber ? (e.target as { valueAsNumber: number }).valueAsNumber : 0; diff --git a/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap index 8a5add971f7f5..ad48ecadd165f 100644 --- a/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap +++ b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PromptPage renders as expected with additional scripts 1`] = `"ElasticMockedFonts

    Some Title

    Some Body
    Action#1
    Action#2
    "`; +exports[`PromptPage renders as expected with additional scripts 1`] = `"ElasticMockedFonts

    Some Title

    Some Body
    Action#1
    Action#2
    "`; -exports[`PromptPage renders as expected without additional scripts 1`] = `"ElasticMockedFonts

    Some Title

    Some Body
    Action#1
    Action#2
    "`; +exports[`PromptPage renders as expected without additional scripts 1`] = `"ElasticMockedFonts

    Some Title

    Some Body
    Action#1
    Action#2
    "`; diff --git a/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap index 7895382d4f654..015467a7c8da5 100644 --- a/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap +++ b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`UnauthenticatedPage renders as expected 1`] = `"ElasticMockedFonts
    "`; +exports[`UnauthenticatedPage renders as expected 1`] = `"ElasticMockedFonts"`; diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap index a19757b3f52b2..604ca460ac67f 100644 --- a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap +++ b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ResetSessionPage renders as expected 1`] = `"ElasticMockedFonts"`; +exports[`ResetSessionPage renders as expected 1`] = `"ElasticMockedFonts"`; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.test.tsx index ec0b95aab778a..89fe0ef8e5c7b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.test.tsx @@ -20,6 +20,6 @@ describe('AnomalyThresholdSlider', () => { }; const wrapper = shallow(); - expect(wrapper.dive().find('EuiRange')).toHaveLength(1); + expect(wrapper.dive().find('EuiRangeClass')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.tsx index 6997777fdc34c..3398b8bec2cf5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/anomaly_threshold_slider/index.tsx @@ -7,6 +7,7 @@ import React, { useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiRange, EuiFormRow } from '@elastic/eui'; +import type { EuiRangeProps } from '@elastic/eui'; import type { FieldHook } from '../../../../shared_imports'; @@ -14,17 +15,15 @@ interface AnomalyThresholdSliderProps { describedByIds: string[]; field: FieldHook; } -type Event = React.ChangeEvent; -type EventArg = Event | React.MouseEvent; export const AnomalyThresholdSlider = ({ describedByIds = [], field, }: AnomalyThresholdSliderProps) => { const threshold = field.value as number; - const onThresholdChange = useCallback( - (event: EventArg) => { - const thresholdValue = Number((event as Event).target.value); + const onThresholdChange: EuiRangeProps['onChange'] = useCallback( + (event) => { + const thresholdValue = Number(event.currentTarget.value); field.setValue(thresholdValue); }, [field] @@ -46,6 +45,8 @@ export const AnomalyThresholdSlider = ({ showRange showTicks tickInterval={25} + min={0} + max={100} /> diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx index 177fd2e97e6cd..f022560f9f50c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx @@ -19,6 +19,7 @@ import { EuiSpacer, EuiRange, } from '@elastic/eui'; +import type { EuiRangeProps } from '@elastic/eui'; import type { DataViewBase, DataViewFieldBase } from '@kbn/es-query'; import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; @@ -67,8 +68,8 @@ export const RiskScoreField = ({ const fieldTypeFilter = useMemo(() => ['number'], []); const selectedField = useMemo(() => getFieldTypeByMapping(mapping, indices), [mapping, indices]); - const handleDefaultRiskScoreChange = useCallback( - (e: React.ChangeEvent | React.MouseEvent): void => { + const handleDefaultRiskScoreChange: EuiRangeProps['onChange'] = useCallback( + (e) => { const range = (e.target as HTMLInputElement).value; setValue({ value: Number(range.trim()), diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx index 099f1b556e601..fb16aaa5bdfbc 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx @@ -144,8 +144,8 @@ export const GraphControls = React.memo( const closePopover = useCallback(() => setPopover(null), []); - const handleZoomAmountChange = useCallback( - (event: React.ChangeEvent | React.MouseEvent) => { + const handleZoomAmountChange: EuiRangeProps['onChange'] = useCallback( + (event) => { const valueAsNumber = parseFloat( (event as React.ChangeEvent).target.value ); diff --git a/x-pack/plugins/session_view/public/components/tty_player/styles.ts b/x-pack/plugins/session_view/public/components/tty_player/styles.ts index 1e38182f94765..4a4fadc7417e1 100644 --- a/x-pack/plugins/session_view/public/components/tty_player/styles.ts +++ b/x-pack/plugins/session_view/public/components/tty_player/styles.ts @@ -26,12 +26,6 @@ export const useStyles = (tty?: Teletype, show?: boolean) => { height: '100%', overflow: 'hidden', zIndex: 10, - '.euiRangeLevel--warning': { - backgroundColor: transparentize(colors.warning, 0.8), - }, - '.euiRangeLevel--danger': { - backgroundColor: transparentize(colors.danger, 0.8), - }, '.euiRangeTick,.euiRangeLevel': { transition: 'left 500ms', }, diff --git a/x-pack/plugins/session_view/public/components/tty_player_controls/index.tsx b/x-pack/plugins/session_view/public/components/tty_player_controls/index.tsx index ebde48b27aad4..af10b030c5149 100644 --- a/x-pack/plugins/session_view/public/components/tty_player_controls/index.tsx +++ b/x-pack/plugins/session_view/public/components/tty_player_controls/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useCallback, ChangeEvent, MouseEvent } from 'react'; +import React, { useCallback } from 'react'; import { EuiButtonEmpty, EuiPanel, @@ -13,6 +13,7 @@ import { EuiButtonIcon, EuiToolTip, EuiButtonIconProps, + EuiRangeProps, } from '@elastic/eui'; import { findIndex } from 'lodash'; import { ProcessStartMarker, ProcessEvent } from '../../../common/types/process_tree'; @@ -62,9 +63,9 @@ export const TTYPlayerControls = ({ css: styles.controlButton, }; - const onLineChange = useCallback( - (event: ChangeEvent | MouseEvent) => { - const line = parseInt((event?.target as HTMLInputElement).value || '0', 10); + const onLineChange: EuiRangeProps['onChange'] = useCallback( + (event) => { + const line = parseInt(event.currentTarget.value || '0', 10); onSeekLine(line); }, [onSeekLine] diff --git a/x-pack/plugins/session_view/public/components/tty_player_controls/tty_player_controls_markers/index.tsx b/x-pack/plugins/session_view/public/components/tty_player_controls/tty_player_controls_markers/index.tsx index e84ed7fcf34a9..0b2223156d634 100644 --- a/x-pack/plugins/session_view/public/components/tty_player_controls/tty_player_controls_markers/index.tsx +++ b/x-pack/plugins/session_view/public/components/tty_player_controls/tty_player_controls_markers/index.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { ChangeEvent, MouseEvent, useMemo } from 'react'; -import { EuiRange, EuiToolTip } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { EuiRange, EuiRangeProps, EuiToolTip } from '@elastic/eui'; import type { ProcessStartMarker } from '../../../../common/types/process_tree'; import { useStyles } from './styles'; import { PlayHead } from './play_head'; @@ -15,7 +15,7 @@ type Props = { processStartMarkers: ProcessStartMarker[]; linesLength: number; currentLine: number; - onChange: (e: ChangeEvent | MouseEvent) => void; + onChange: EuiRangeProps['onChange']; onSeekLine(line: number): void; }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_locations.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_locations.tsx index c1ea2e8c537af..dc9792bab30c0 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_locations.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_locations.tsx @@ -82,7 +82,7 @@ function getLocationStatusColor( overviewStatus: OverviewStatusState | null ) { const { - eui: { euiColorVis9, euiColorVis0, euiSideNavDisabledTextcolor }, + eui: { euiColorVis9, euiColorVis0, euiColorDisabled }, } = euiTheme; const locById = `${monitorId}-${locationLabel}`; @@ -93,5 +93,5 @@ function getLocationStatusColor( return euiColorVis0; } - return euiSideNavDisabledTextcolor; + return euiColorDisabled; } diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/common/charts/__snapshots__/donut_chart.test.tsx.snap b/x-pack/plugins/synthetics/public/legacy_uptime/components/common/charts/__snapshots__/donut_chart.test.tsx.snap index 9cd19cb2404de..58e01666035e8 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/common/charts/__snapshots__/donut_chart.test.tsx.snap +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/common/charts/__snapshots__/donut_chart.test.tsx.snap @@ -534,7 +534,7 @@ exports[`DonutChart component passes correct props without errors for valid prop "strokeWidth": 2, }, "point": Object { - "fill": "rgba(255, 255, 255, 1)", + "fill": "#FFF", "radius": 3, "strokeWidth": 2, "visible": false, @@ -542,10 +542,10 @@ exports[`DonutChart component passes correct props without errors for valid prop }, "axes": Object { "axisLine": Object { - "stroke": "rgba(238, 240, 243, 1)", + "stroke": "#eaedf3", }, "axisTitle": Object { - "fill": "rgba(52, 55, 65, 1)", + "fill": "#343741", "fontFamily": "'Inter', 'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", "fontSize": 12, @@ -561,7 +561,7 @@ exports[`DonutChart component passes correct props without errors for valid prop 0, ], "opacity": 1, - "stroke": "rgba(238, 240, 243, 1)", + "stroke": "#eaedf3", "strokeWidth": 1, "visible": true, }, @@ -571,13 +571,13 @@ exports[`DonutChart component passes correct props without errors for valid prop 4, ], "opacity": 1, - "stroke": "rgba(238, 240, 243, 1)", + "stroke": "#eaedf3", "strokeWidth": 1, "visible": true, }, }, "tickLabel": Object { - "fill": "rgba(105, 112, 125, 1)", + "fill": "#646a77", "fontFamily": "'Inter', 'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", "fontSize": 10, @@ -587,13 +587,13 @@ exports[`DonutChart component passes correct props without errors for valid prop }, }, "tickLine": Object { - "stroke": "rgba(238, 240, 243, 1)", + "stroke": "#eaedf3", "strokeWidth": 1, "visible": false, }, }, "background": Object { - "color": "rgba(255, 255, 255, 1)", + "color": "#FFF", }, "barSeriesStyle": Object { "displayValue": Object { @@ -632,14 +632,14 @@ exports[`DonutChart component passes correct props without errors for valid prop }, "crosshair": Object { "band": Object { - "fill": "rgba(245, 247, 250, 1)", + "fill": "#F1F4FA", }, "crossLine": Object { "dash": Array [ 4, 4, ], - "stroke": "rgba(105, 112, 125, 1)", + "stroke": "#69707D", "strokeWidth": 1, }, "line": Object { @@ -647,44 +647,44 @@ exports[`DonutChart component passes correct props without errors for valid prop 4, 4, ], - "stroke": "rgba(105, 112, 125, 1)", + "stroke": "#69707D", "strokeWidth": 1, }, }, "goal": Object { "majorCenterLabel": Object { - "fill": "rgba(52, 55, 65, 1)", + "fill": "#343741", "fontFamily": "'Inter', 'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", }, "majorLabel": Object { - "fill": "rgba(52, 55, 65, 1)", + "fill": "#343741", "fontFamily": "'Inter', 'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", }, "minorCenterLabel": Object { - "fill": "rgba(105, 112, 125, 1)", + "fill": "#646a77", "fontFamily": "'Inter', 'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", }, "minorLabel": Object { - "fill": "rgba(105, 112, 125, 1)", + "fill": "#646a77", "fontFamily": "'Inter', 'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", }, "progressLine": Object { - "stroke": "rgba(52, 55, 65, 1)", + "stroke": "#343741", }, "targetLine": Object { - "stroke": "rgba(52, 55, 65, 1)", + "stroke": "#343741", }, "tickLabel": Object { - "fill": "rgba(105, 112, 125, 1)", + "fill": "#646a77", "fontFamily": "'Inter', 'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", }, "tickLine": Object { - "stroke": "rgba(152, 162, 179, 1)", + "stroke": "#98A2B3", }, }, "lineSeriesStyle": Object { @@ -692,7 +692,7 @@ exports[`DonutChart component passes correct props without errors for valid prop "strokeWidth": 2, }, "point": Object { - "fill": "rgba(255, 255, 255, 1)", + "fill": "#FFF", "radius": 3, "strokeWidth": 2, }, @@ -709,12 +709,12 @@ exports[`DonutChart component passes correct props without errors for valid prop "linkLabel": Object { "fontSize": 11, "maxCount": 5, - "textColor": "rgba(52, 55, 65, 1)", + "textColor": "#343741", }, "maxFontSize": 16, "minFontSize": 8, "outerSizeRatio": 1, - "sectorLineStroke": "rgba(255, 255, 255, 1)", + "sectorLineStroke": "#FFF", "sectorLineWidth": 1.5, }, "scales": Object { diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index c72f98d594828..21dd61e9d5249 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -64,7 +64,7 @@ export default function ({ getService }: FtrProviderContext) { // markers { color: '#5078AA', percentage: 15 }, // grey boilerplate - { color: '#6A717D', percentage: 12 }, + { color: '#69707D', percentage: 12 }, ], scatterplotMatrixColorStatsResults: [ // outlier markers @@ -72,7 +72,7 @@ export default function ({ getService }: FtrProviderContext) { // regular markers { color: '#6496BE', percentage: 15 }, // tick/grid/axis - { color: '#6E6E82', percentage: 11 }, + { color: '#69707D', percentage: 12 }, { color: '#D2DCE6', percentage: 10 }, // anti-aliasing { color: '#F5F7FA', percentage: 35 }, diff --git a/yarn.lock b/yarn.lock index 69a3a6ae1ab01..8c63ce46798f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1527,10 +1527,10 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@70.4.0": - version "70.4.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-70.4.0.tgz#0ce7520ac96e137f05861224a6cd0a029c4dc0bc" - integrity sha512-w/pMxC0drBtzy3RQzHBLLbKRgy4EUTSetej0eg7m87copRZOwWXqlrIt52uuUj9txenxmpSonnnvSB+1a7fCfg== +"@elastic/eui@71.0.0": + version "71.0.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-71.0.0.tgz#0d77ca3e513ebd59fee0be49abd7acf4b04206aa" + integrity sha512-5iHvGSJCJjin/VPHBT0RdHVKUCClH5PqXnygsr6LCkyQzj+frKiK0w28dK5EBReDp5+SRoF+VYSVse4Ia2DkLQ== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.160" From e4f4133ab7e1b2cd9f0d39a506555e5015ea585c Mon Sep 17 00:00:00 2001 From: Georgii Gorbachev Date: Mon, 19 Dec 2022 22:49:00 +0100 Subject: [PATCH 32/55] [Security Solution] Fix rule status refresh/loading indicator on the Rule Details page (#147806) **Fixes:** https://github.com/elastic/kibana/issues/146362 ## Summary With normal network settings and then with "Slow 3G" network throttling: https://user-images.githubusercontent.com/7359339/208510625-f9cbcff2-bc52-4e00-87aa-146e94abf7f3.mov ### Checklist - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../rule_management/logic/use_rule_with_fallback.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_rule_with_fallback.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_rule_with_fallback.ts index e9004d80a5f0a..4ab65ae0fd8c7 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_rule_with_fallback.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_rule_with_fallback.ts @@ -77,7 +77,7 @@ const buildLastAlertQuery = (ruleId: string) => ({ * In that case, try to fetch the latest alert generated by the rule and retrieve the rule data from the alert (fallback). */ export const useRuleWithFallback = (ruleId: string): UseRuleWithFallback => { - const { isLoading: ruleLoading, data: ruleData, error, refetch } = useRule(ruleId, false); + const { isFetching: ruleLoading, data: ruleData, error, refetch } = useRule(ruleId, false); const { addError } = useAppToasts(); const isExistingRule = !isNotFoundError(error); From 7a6eac8d1bfe492b8dce83c7a0f47dc26706e388 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Mon, 19 Dec 2022 14:55:31 -0700 Subject: [PATCH 33/55] [Dashboard] [Controls] Allow control changes to be discarded (#147482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/elastic/kibana/issues/147293 ## Summary Before this change, the Redux state `explicitInput` was getting out of sync with the embeddable `explicitInput` in scenarios where the new `explicitInput` was missing a key that the old `explicitInput` had - therefore, because they were out of sync, the changes that **should** have been discarded kept getting injected back into the embeddable `explicitInput`, which made it impossible to actually discard anything unless the key existed in both the before and after state. This PR fixes this by replacing the entire Redux state `explicitInput` with the embeddable `explicitInput` rather than spreading the new value. It also fixes a bug with the time slider control where changes to the embeddable's input were not reflected properly in the control's state, so nothing could be discarded even after the initial bug was fixed. #### Further Explanation When a control is first created, all the optional properties of the explicit input do not yet exist - for example, when creating an options list control, the `selections` key does not exist in the `explicitInput` until a selection is made. Therefore, imagine the following scenario: 1. You create an options list control (where the `selections` key does not exist) and save the dashboard 2. You make some selections, which causes `unsaved changes` because the `selections` key now exists and is equal to an array 3. You switch to view mode and choose to discard your changes, thus (supposedly) removing the `selections` key from the `explicitInput` object once again Unfortunately, the Redux embeddable state for each control was **not** accurately removing the `selections` key as expected - this was because, when trying to update the `explicitInput` via the old `updateEmbeddableReduxInput`, the new value was **spread** on top of the older value rather than replacing it. In a simplified scenario, this resulted in something like this: ```typescript const oldExplicitInput = { id: 'test_id', selections: ['test selection'] }; const newExplicitInput = { id: 'test_id' } const result = { ...oldExplicitInput, ...newExplicitInput }; ``` In this code, because `newExplicitInput` does not have the `selections` key, `result` will equal `{ id: 'test_id', selections: ['test selection'] }` - this is not the behaviour we want! Instead, we wanted to replace the entire old `explicitInput` with the new `explicitInput`. Effectively, that is what this PR does. Thanks to @ThomThomson for helping out with finding the root cause of this after I got lost :) ### How to Test For both options list and range slider controls, 1. Create a control of the desired type 2. Save the dashboard 3. Make some sort of change that causes unsaved changes - for example, make a selection or, if an options list control, set `exclude` to `true` 4. Switch to view mode, discarding the changes 5. Ensure that the changes made in step 3 are no longer applied ✅ 6. Switch back to edit mode 7. Ensure that there are no `unsaved changes` ✅ #### Flaky Test Runner ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../embeddable/time_slider_embeddable.tsx | 53 ++++++++++++++++--- .../create_redux_embeddable_tools.tsx | 12 ++--- .../sync_redux_embeddable.ts | 4 +- .../controls/options_list.ts | 17 ++++++ .../controls/range_slider.ts | 21 ++++++++ .../controls/time_slider.ts | 28 +++++++++- .../page_objects/dashboard_page_controls.ts | 17 ++++++ 7 files changed, 134 insertions(+), 18 deletions(-) diff --git a/src/plugins/controls/public/time_slider/embeddable/time_slider_embeddable.tsx b/src/plugins/controls/public/time_slider/embeddable/time_slider_embeddable.tsx index e12320356dd66..a927843ca1337 100644 --- a/src/plugins/controls/public/time_slider/embeddable/time_slider_embeddable.tsx +++ b/src/plugins/controls/public/time_slider/embeddable/time_slider_embeddable.tsx @@ -50,6 +50,10 @@ export class TimeSliderControlEmbeddable extends Embeddable< private getTimezone: ControlsSettingsService['getTimezone']; private timefilter: ControlsDataService['timefilter']; private prevTimeRange: TimeRange | undefined; + private prevTimesliceAsPercentage: { + timesliceStartAsPercentageOfTimeRange?: number; + timesliceEndAsPercentageOfTimeRange?: number; + }; private readonly waitForControlOutputConsumersToLoad$; private reduxEmbeddableTools: ReduxEmbeddableTools< @@ -112,6 +116,10 @@ export class TimeSliderControlEmbeddable extends Embeddable< ) : undefined; + this.prevTimesliceAsPercentage = { + timesliceStartAsPercentageOfTimeRange: this.getInput().timesliceStartAsPercentageOfTimeRange, + timesliceEndAsPercentageOfTimeRange: this.getInput().timesliceEndAsPercentageOfTimeRange, + }; this.syncWithTimeRange(); } @@ -125,13 +133,29 @@ export class TimeSliderControlEmbeddable extends Embeddable< private onInputChange() { const input = this.getInput(); + const { timesliceStartAsPercentageOfTimeRange, timesliceEndAsPercentageOfTimeRange } = + this.prevTimesliceAsPercentage ?? {}; - if (!input.timeRange) { - return; - } - - if (!_.isEqual(input.timeRange, this.prevTimeRange)) { - const { actions, dispatch } = this.reduxEmbeddableTools; + const { actions, dispatch } = this.reduxEmbeddableTools; + if ( + timesliceStartAsPercentageOfTimeRange !== input.timesliceStartAsPercentageOfTimeRange || + timesliceEndAsPercentageOfTimeRange !== input.timesliceEndAsPercentageOfTimeRange + ) { + // Discarding edit mode changes results in replacing edited input with original input + // Re-sync with time range when edited input timeslice changes are discarded + if ( + !input.timesliceStartAsPercentageOfTimeRange && + !input.timesliceEndAsPercentageOfTimeRange + ) { + // If no selections have been saved into the timeslider, then both `timesliceStartAsPercentageOfTimeRange` + // and `timesliceEndAsPercentageOfTimeRange` will be undefined - so, need to reset component state to match + dispatch(actions.publishValue({ value: undefined })); + dispatch(actions.setValue({ value: undefined })); + } else { + // Otherwise, need to call `syncWithTimeRange` so that the component state value can be calculated and set + this.syncWithTimeRange(); + } + } else if (input.timeRange && !_.isEqual(input.timeRange, this.prevTimeRange)) { const nextBounds = this.timeRangeToBounds(input.timeRange); const ticks = getTicks(nextBounds[FROM_INDEX], nextBounds[TO_INDEX], this.getTimezone()); dispatch( @@ -153,6 +177,7 @@ export class TimeSliderControlEmbeddable extends Embeddable< getState().explicitInput.timesliceStartAsPercentageOfTimeRange; const timesliceEndAsPercentageOfTimeRange = getState().explicitInput.timesliceEndAsPercentageOfTimeRange; + if ( timesliceStartAsPercentageOfTimeRange !== undefined && timesliceEndAsPercentageOfTimeRange !== undefined @@ -187,8 +212,8 @@ export class TimeSliderControlEmbeddable extends Embeddable< dispatch(actions.publishValue({ value })); }, 500); - private onTimesliceChange = (value?: [number, number]) => { - const { actions, dispatch, getState } = this.reduxEmbeddableTools; + private getTimeSliceAsPercentageOfTimeRange(value?: [number, number]) { + const { getState } = this.reduxEmbeddableTools; let timesliceStartAsPercentageOfTimeRange: number | undefined; let timesliceEndAsPercentageOfTimeRange: number | undefined; if (value) { @@ -199,6 +224,18 @@ export class TimeSliderControlEmbeddable extends Embeddable< timesliceEndAsPercentageOfTimeRange = (value[TO_INDEX] - timeRangeBounds[FROM_INDEX]) / timeRange; } + this.prevTimesliceAsPercentage = { + timesliceStartAsPercentageOfTimeRange, + timesliceEndAsPercentageOfTimeRange, + }; + return { timesliceStartAsPercentageOfTimeRange, timesliceEndAsPercentageOfTimeRange }; + } + + private onTimesliceChange = (value?: [number, number]) => { + const { actions, dispatch } = this.reduxEmbeddableTools; + + const { timesliceStartAsPercentageOfTimeRange, timesliceEndAsPercentageOfTimeRange } = + this.getTimeSliceAsPercentageOfTimeRange(value); dispatch( actions.setValueAsPercentageOfTimeRange({ timesliceStartAsPercentageOfTimeRange, diff --git a/src/plugins/presentation_util/public/redux_embeddables/create_redux_embeddable_tools.tsx b/src/plugins/presentation_util/public/redux_embeddables/create_redux_embeddable_tools.tsx index 426cb073b55fb..ed7938a0760b4 100644 --- a/src/plugins/presentation_util/public/redux_embeddables/create_redux_embeddable_tools.tsx +++ b/src/plugins/presentation_util/public/redux_embeddables/create_redux_embeddable_tools.tsx @@ -52,17 +52,17 @@ export const createReduxEmbeddableTools = < }): ReduxEmbeddableTools => { // Additional generic reducers to aid in embeddable syncing const genericReducers = { - updateEmbeddableReduxInput: ( + replaceEmbeddableReduxInput: ( state: Draft, - action: PayloadAction> + action: PayloadAction ) => { - state.explicitInput = { ...state.explicitInput, ...action.payload }; + state.explicitInput = action.payload; }, - updateEmbeddableReduxOutput: ( + replaceEmbeddableReduxOutput: ( state: Draft, - action: PayloadAction> + action: PayloadAction ) => { - state.output = { ...state.output, ...action.payload }; + state.output = action.payload; }, }; diff --git a/src/plugins/presentation_util/public/redux_embeddables/sync_redux_embeddable.ts b/src/plugins/presentation_util/public/redux_embeddables/sync_redux_embeddable.ts index 5f4dd20818ba2..37aadd1e9fe47 100644 --- a/src/plugins/presentation_util/public/redux_embeddables/sync_redux_embeddable.ts +++ b/src/plugins/presentation_util/public/redux_embeddables/sync_redux_embeddable.ts @@ -81,7 +81,7 @@ export const syncReduxEmbeddable = < if (!inputEqual(reduxExplicitInput, embeddableExplictInput)) { store.dispatch( - actions.updateEmbeddableReduxInput(cleanInputForRedux(embeddableExplictInput)) + actions.replaceEmbeddableReduxInput(cleanInputForRedux(embeddableExplictInput)) ); } embeddableToReduxInProgress = false; @@ -93,7 +93,7 @@ export const syncReduxEmbeddable = < embeddableToReduxInProgress = true; const reduxState = store.getState(); if (!outputEqual(reduxState.output, embeddableOutput)) { - store.dispatch(actions.updateEmbeddableReduxOutput(embeddableOutput)); + store.dispatch(actions.replaceEmbeddableReduxOutput(embeddableOutput)); } embeddableToReduxInProgress = false; }); diff --git a/test/functional/apps/dashboard_elements/controls/options_list.ts b/test/functional/apps/dashboard_elements/controls/options_list.ts index 9ea4193ce23df..09c14f1804566 100644 --- a/test/functional/apps/dashboard_elements/controls/options_list.ts +++ b/test/functional/apps/dashboard_elements/controls/options_list.ts @@ -439,6 +439,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await pieChart.getPieSliceCount()).to.be(2); await dashboard.clearUnsavedChanges(); }); + + it('changes to selections can be discarded', async () => { + await dashboardControls.optionsListOpenPopover(controlId); + await dashboardControls.optionsListPopoverSelectOption('bark'); + await dashboardControls.optionsListEnsurePopoverIsClosed(controlId); + let selections = await dashboardControls.optionsListGetSelectionsString(controlId); + expect(selections).to.equal('hiss, grr, bark'); + + await dashboard.clickCancelOutOfEditMode(); + selections = await dashboardControls.optionsListGetSelectionsString(controlId); + expect(selections).to.equal('hiss, grr'); + }); + + it('dashboard does not load with unsaved changes when changes are discarded', async () => { + await dashboard.switchToEditMode(); + await testSubjects.missingOrFail('dashboardUnsavedChangesBadge'); + }); }); describe('test data view runtime field', async () => { diff --git a/test/functional/apps/dashboard_elements/controls/range_slider.ts b/test/functional/apps/dashboard_elements/controls/range_slider.ts index e75c83c53bc18..17d787b55c5d1 100644 --- a/test/functional/apps/dashboard_elements/controls/range_slider.ts +++ b/test/functional/apps/dashboard_elements/controls/range_slider.ts @@ -183,6 +183,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const firstId = (await dashboardControls.getAllControlIds())[0]; await dashboardControls.rangeSliderClearSelection(firstId); await dashboardControls.validateRange('value', firstId, '', ''); + await dashboard.clearUnsavedChanges(); + }); + + it('making changes to range causes unsaved changes', async () => { + const firstId = (await dashboardControls.getAllControlIds())[0]; + await dashboardControls.rangeSliderSetLowerBound(firstId, '0'); + await dashboardControls.rangeSliderSetUpperBound(firstId, '3'); + await dashboardControls.rangeSliderWaitForLoading(); + await testSubjects.existOrFail('dashboardUnsavedChangesBadge'); + }); + + it('changes to range can be discarded', async () => { + const firstId = (await dashboardControls.getAllControlIds())[0]; + await dashboardControls.validateRange('value', firstId, '0', '3'); + await dashboard.clickCancelOutOfEditMode(); + await dashboardControls.validateRange('value', firstId, '', ''); + }); + + it('dashboard does not load with unsaved changes when changes are discarded', async () => { + await dashboard.switchToEditMode(); + await testSubjects.missingOrFail('dashboardUnsavedChangesBadge'); }); it('deletes an existing control', async () => { diff --git a/test/functional/apps/dashboard_elements/controls/time_slider.ts b/test/functional/apps/dashboard_elements/controls/time_slider.ts index 03331f97827d9..d2cff619282b8 100644 --- a/test/functional/apps/dashboard_elements/controls/time_slider.ts +++ b/test/functional/apps/dashboard_elements/controls/time_slider.ts @@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const security = getService('security'); const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); + const { dashboardControls, discover, timePicker, common, dashboard } = getPageObjects([ 'dashboardControls', 'discover', @@ -52,7 +53,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.testUser.restoreDefaults(); }); - describe('create and delete', async () => { + describe('create, edit, and delete', async () => { before(async () => { await common.navigateToApp('dashboard'); await dashboard.preserveCrossAppState(); @@ -62,11 +63,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'Oct 22, 2018 @ 00:00:00.000', 'Dec 3, 2018 @ 00:00:00.000' ); + await dashboard.saveDashboard('test time slider control', { exitFromEditMode: false }); }); it('can create a new time slider control from a blank state', async () => { await dashboardControls.createTimeSliderControl(); expect(await dashboardControls.getControlsCount()).to.be(1); + await dashboard.clearUnsavedChanges(); }); it('can not add a second time slider control', async () => { @@ -87,13 +90,34 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardControls.validateRange('placeholder', secondId, '100', '1200'); }); - it('applies filter from the first control on the second control', async () => { + it('making changes to time slice causes unsaved changes', async () => { await dashboardControls.gotoNextTimeSlice(); + await dashboard.clearUnsavedChanges(); + }); + + it('applies filter from the first control on the second control', async () => { await dashboardControls.rangeSliderWaitForLoading(); const secondId = (await dashboardControls.getAllControlIds())[1]; await dashboardControls.validateRange('placeholder', secondId, '101', '1000'); }); + it('changes to time slice can be discarded', async () => { + const valueBefore = await dashboardControls.getTimeSliceFromTimeSlider(); + await dashboardControls.gotoNextTimeSlice(); + const valueAfter = await dashboardControls.getTimeSliceFromTimeSlider(); + expect(valueBefore).to.not.equal(valueAfter); + + await dashboardControls.closeTimeSliderPopover(); + await dashboard.clickCancelOutOfEditMode(); + const valueNow = await dashboardControls.getTimeSliceFromTimeSlider(); + expect(valueNow).to.equal(valueBefore); + }); + + it('dashboard does not load with unsaved changes when changes are discarded', async () => { + await dashboard.switchToEditMode(); + await testSubjects.missingOrFail('dashboardUnsavedChangesBadge'); + }); + it('deletes an existing control', async () => { const firstId = (await dashboardControls.getAllControlIds())[0]; await dashboardControls.removeExistingControl(firstId); diff --git a/test/functional/page_objects/dashboard_page_controls.ts b/test/functional/page_objects/dashboard_page_controls.ts index e2b91346d15ee..a584fbed741e9 100644 --- a/test/functional/page_objects/dashboard_page_controls.ts +++ b/test/functional/page_objects/dashboard_page_controls.ts @@ -615,4 +615,21 @@ export class DashboardPageControls extends FtrService { await this.testSubjects.click('timeSlider-popoverToggleButton'); } } + + public async getTimeSliceFromTimeSlider() { + const isOpen = await this.testSubjects.exists('timeSlider-popoverContents'); + if (!isOpen) { + await this.testSubjects.click('timeSlider-popoverToggleButton'); + await this.retry.try(async () => { + await this.testSubjects.existOrFail('timeSlider-popoverContents'); + }); + } + const popover = await this.testSubjects.find('timeSlider-popoverContents'); + const dualRangeSlider = await this.find.descendantDisplayedByCssSelector( + '.euiRangeDraggable', + popover + ); + const value = await dualRangeSlider.getAttribute('aria-valuetext'); + return value; + } } From 0b19cfafa3dba454148008cae2c7f651bbe15846 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Mon, 19 Dec 2022 16:37:39 -0800 Subject: [PATCH 34/55] Custom fleet policy UX for new integration (cloud defend v1) (#147300) ## Summary New Kibana plugin created for an integration called "Cloud defend for containers" which will have a corresponding agent service which can proactively block and alert on executable creation or modification in a running container. This plugin is purely in place to configure the fleet policy UX around this new integration. For now we have added a yaml editor as a custom input to our integration. The monaco-yaml libary was added to allow support for JSON schema validation support for yaml. Integration PR is up, and a work in progress: (waiting on some content for the doc page) https://github.com/elastic/integrations/pull/4680 ### Screenshot ![image](https://user-images.githubusercontent.com/16198204/207160791-73e11e05-953b-42ba-b4dd-a4904bd95451.png) ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) Co-authored-by: Karl Godard Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- docs/developer/plugin-list.asciidoc | 4 + package.json | 1 + packages/kbn-monaco/BUILD.bazel | 1 + packages/kbn-monaco/src/monaco_imports.ts | 1 + packages/kbn-monaco/src/register_globals.ts | 8 +- packages/kbn-monaco/webpack.config.js | 4 +- packages/kbn-optimizer/limits.yml | 1 + packages/kbn-test/jest-preset.js | 2 +- renovate.json | 9 ++ tsconfig.base.json | 2 + x-pack/.i18nrc.json | 1 + x-pack/plugins/cloud_defend/.i18nrc.json | 7 + x-pack/plugins/cloud_defend/README.md | 48 ++++++ .../plugins/cloud_defend/common/constants.ts | 12 ++ x-pack/plugins/cloud_defend/jest.config.js | 18 +++ x-pack/plugins/cloud_defend/kibana.json | 14 ++ .../cloud_defend/public/common/utils.ts | 12 ++ .../__mocks__/resizeobserver.js | 21 +++ .../config_yaml_view/__mocks__/worker.js | 22 +++ .../hooks/use_config_model.ts | 150 ++++++++++++++++++ .../config_yaml_view/index.test.tsx | 62 ++++++++ .../components/config_yaml_view/index.tsx | 124 +++++++++++++++ .../components/config_yaml_view/mocks.ts | 116 ++++++++++++++ .../components/config_yaml_view/styles.ts | 19 +++ .../config_yaml_view/translations.ts | 26 +++ .../policy_extension_create.tsx | 20 +++ .../policy_extension_edit.tsx | 20 +++ x-pack/plugins/cloud_defend/public/index.ts | 14 ++ x-pack/plugins/cloud_defend/public/plugin.ts | 44 +++++ .../public/test/test_provider.tsx | 48 ++++++ .../plugins/cloud_defend/public/test/utils.ts | 17 ++ x-pack/plugins/cloud_defend/public/types.ts | 20 +++ x-pack/plugins/cloud_defend/tsconfig.json | 17 ++ yarn.lock | 39 ++++- 34 files changed, 918 insertions(+), 6 deletions(-) create mode 100755 x-pack/plugins/cloud_defend/.i18nrc.json create mode 100755 x-pack/plugins/cloud_defend/README.md create mode 100755 x-pack/plugins/cloud_defend/common/constants.ts create mode 100644 x-pack/plugins/cloud_defend/jest.config.js create mode 100755 x-pack/plugins/cloud_defend/kibana.json create mode 100644 x-pack/plugins/cloud_defend/public/common/utils.ts create mode 100644 x-pack/plugins/cloud_defend/public/components/config_yaml_view/__mocks__/resizeobserver.js create mode 100644 x-pack/plugins/cloud_defend/public/components/config_yaml_view/__mocks__/worker.js create mode 100644 x-pack/plugins/cloud_defend/public/components/config_yaml_view/hooks/use_config_model.ts create mode 100644 x-pack/plugins/cloud_defend/public/components/config_yaml_view/index.test.tsx create mode 100644 x-pack/plugins/cloud_defend/public/components/config_yaml_view/index.tsx create mode 100644 x-pack/plugins/cloud_defend/public/components/config_yaml_view/mocks.ts create mode 100644 x-pack/plugins/cloud_defend/public/components/config_yaml_view/styles.ts create mode 100644 x-pack/plugins/cloud_defend/public/components/config_yaml_view/translations.ts create mode 100644 x-pack/plugins/cloud_defend/public/components/fleet_extensions/policy_extension_create.tsx create mode 100644 x-pack/plugins/cloud_defend/public/components/fleet_extensions/policy_extension_edit.tsx create mode 100755 x-pack/plugins/cloud_defend/public/index.ts create mode 100755 x-pack/plugins/cloud_defend/public/plugin.ts create mode 100755 x-pack/plugins/cloud_defend/public/test/test_provider.tsx create mode 100644 x-pack/plugins/cloud_defend/public/test/utils.ts create mode 100755 x-pack/plugins/cloud_defend/public/types.ts create mode 100755 x-pack/plugins/cloud_defend/tsconfig.json diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index f726d3b686d82..814beb270c7fd 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -449,6 +449,10 @@ The plugin exposes the static DefaultEditorController class to consume. |Static migration page where self-managed users can see text/copy about migrating to Elastic Cloud +|{kib-repo}blob/{branch}/x-pack/plugins/cloud_defend/README.md[cloudDefend] +|This plugin currently only exists to provide custom fleet policy UX for a set of new BPF LSM features. The first feature being container "drift prevention". + + |{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_experiments/README.mdx[cloudExperiments] |The Cloud Experiments Service provides the necessary APIs to implement A/B testing scenarios, fetching the variations in configuration and reporting back metrics to track conversion rates of the experiments. diff --git a/package.json b/package.json index 851824b97b505..f74c286cfc336 100644 --- a/package.json +++ b/package.json @@ -578,6 +578,7 @@ "moment-duration-format": "^2.3.2", "moment-timezone": "^0.5.34", "monaco-editor": "^0.24.0", + "monaco-yaml": "3.2.1", "mustache": "^2.3.2", "node-fetch": "^2.6.7", "node-forge": "^1.3.1", diff --git a/packages/kbn-monaco/BUILD.bazel b/packages/kbn-monaco/BUILD.bazel index 541ba68265771..35da648bccb59 100644 --- a/packages/kbn-monaco/BUILD.bazel +++ b/packages/kbn-monaco/BUILD.bazel @@ -47,6 +47,7 @@ RUNTIME_DEPS = [ "@npm//antlr4ts", "@npm//babel-loader", "@npm//monaco-editor", + "@npm//monaco-yaml", "@npm//raw-loader", "@npm//rxjs", ] diff --git a/packages/kbn-monaco/src/monaco_imports.ts b/packages/kbn-monaco/src/monaco_imports.ts index 07a24c8c8bd2e..9da2a3f4562f3 100644 --- a/packages/kbn-monaco/src/monaco_imports.ts +++ b/packages/kbn-monaco/src/monaco_imports.ts @@ -26,5 +26,6 @@ import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/bracketMatching.js'; import 'monaco-editor/esm/vs/language/json/monaco.contribution.js'; import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js'; // Needed for basic javascript support import 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution.js'; // Needed for basic xml support +import 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution'; // Needed for yaml support export { monaco }; diff --git a/packages/kbn-monaco/src/register_globals.ts b/packages/kbn-monaco/src/register_globals.ts index 21b4b8d92cf74..7754b140305aa 100644 --- a/packages/kbn-monaco/src/register_globals.ts +++ b/packages/kbn-monaco/src/register_globals.ts @@ -11,12 +11,13 @@ import { PainlessLang } from './painless'; import { SQLLang } from './sql'; import { monaco } from './monaco_imports'; import { ESQL_THEME_ID, ESQLLang, buildESQlTheme } from './esql'; - import { registerLanguage, registerTheme } from './helpers'; import { createWorkersRegistry } from './workers_registry'; export const DEFAULT_WORKER_ID = 'default'; +const Yaml = 'yaml'; + const workerRegistry = createWorkersRegistry(DEFAULT_WORKER_ID); workerRegistry.register( @@ -44,6 +45,11 @@ workerRegistry.register( async () => await import('!!raw-loader!../../target_workers/json.editor.worker.js') ); +workerRegistry.register( + Yaml, + async () => await import('!!raw-loader!../../target_workers/yaml.editor.worker.js') +); + /** * Register languages and lexer rules */ diff --git a/packages/kbn-monaco/webpack.config.js b/packages/kbn-monaco/webpack.config.js index 15af5ad2f8e89..f15f48ac10da5 100644 --- a/packages/kbn-monaco/webpack.config.js +++ b/packages/kbn-monaco/webpack.config.js @@ -14,6 +14,8 @@ const getWorkerEntry = (language) => { return 'monaco-editor/esm/vs/editor/editor.worker.js'; case 'json': return 'monaco-editor/esm/vs/language/json/json.worker.js'; + case 'yaml': + return 'monaco-yaml/lib/esm/yaml.worker.js'; default: return path.resolve(__dirname, 'src', language, 'worker', `${language}.worker.ts`); } @@ -47,4 +49,4 @@ const getWorkerConfig = (language) => ({ }, }); -module.exports = ['default', 'json', 'painless', 'xjson', 'esql'].map(getWorkerConfig); +module.exports = ['default', 'json', 'painless', 'xjson', 'esql', 'yaml'].map(getWorkerConfig); diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index fc0b4fc3d0831..c1371edbd4068 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -12,6 +12,7 @@ pageLoadAssetSize: cloud: 21076 cloudChat: 19894 cloudDataMigration: 19170 + cloudDefend: 18697 cloudExperiments: 59358 cloudFullStory: 18493 cloudGainsight: 18710 diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index fc6f2cbfdce8c..e9031f0d022be 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -132,7 +132,7 @@ module.exports = { transformIgnorePatterns: [ // ignore all node_modules except monaco-editor and react-monaco-editor which requires babel transforms to handle dynamic import() // since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842) - '[/\\\\]node_modules(?![\\/\\\\](byte-size|monaco-editor|react-monaco-editor|d3-interpolate|d3-color))[/\\\\].+\\.js$', + '[/\\\\]node_modules(?![\\/\\\\](byte-size|monaco-editor|monaco-yaml|vscode-languageserver-types|react-monaco-editor|d3-interpolate|d3-color))[/\\\\].+\\.js$', 'packages/kbn-pm/dist/index.js', ], diff --git a/renovate.json b/renovate.json index 801f2f0b0ceae..87e3ca8986be4 100644 --- a/renovate.json +++ b/renovate.json @@ -259,6 +259,15 @@ "enabled": true, "prCreation": "immediate" }, + { + "groupName": "Cloud Defend", + "matchPackageNames": ["monaco-yaml"], + "reviewers": ["team:sec-cloudnative-integrations"], + "matchBaseBranches": ["main"], + "labels": ["Team: Cloud Native Integrations", "release_note:skip", "backport:skip"], + "enabled": true, + "prCreation": "immediate" + }, { "groupName": "XState", "matchPackageNames": ["xstate"], diff --git a/tsconfig.base.json b/tsconfig.base.json index d4f127712d496..8b656877ab7a7 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1072,6 +1072,8 @@ "@kbn/canvas-plugin/*": ["x-pack/plugins/canvas/*"], "@kbn/cases-plugin": ["x-pack/plugins/cases"], "@kbn/cases-plugin/*": ["x-pack/plugins/cases/*"], + "@kbn/cloud-defend-plugin": ["x-pack/plugins/cloud_defend"], + "@kbn/cloud-defend-plugin/*": ["x-pack/plugins/cloud_defend/*"], "@kbn/cloud-chat-plugin": ["x-pack/plugins/cloud_integrations/cloud_chat"], "@kbn/cloud-chat-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_chat/*"], "@kbn/cloud-data-migration-plugin": ["x-pack/plugins/cloud_integrations/cloud_data_migration"], diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index a27882c5bfc62..6e214dcb448e8 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -11,6 +11,7 @@ "xpack.cases": "plugins/cases", "xpack.cloud": "plugins/cloud", "xpack.cloudChat": "plugins/cloud_integrations/cloud_chat", + "xpack.cloudDefend": "plugins/cloud_defend", "xpack.cloudLinks": "plugins/cloud_integrations/cloud_links", "xpack.cloudDataMigration": "plugins/cloud_integrations/cloud_data_migration", "xpack.csp": "plugins/cloud_security_posture", diff --git a/x-pack/plugins/cloud_defend/.i18nrc.json b/x-pack/plugins/cloud_defend/.i18nrc.json new file mode 100755 index 0000000000000..c1b9cf0f87f0c --- /dev/null +++ b/x-pack/plugins/cloud_defend/.i18nrc.json @@ -0,0 +1,7 @@ +{ + "prefix": "cloudDefend", + "paths": { + "cloudDefend": "." + }, + "translations": ["translations/ja-JP.json"] +} diff --git a/x-pack/plugins/cloud_defend/README.md b/x-pack/plugins/cloud_defend/README.md new file mode 100755 index 0000000000000..c0175af9cc2a6 --- /dev/null +++ b/x-pack/plugins/cloud_defend/README.md @@ -0,0 +1,48 @@ +# Cloud Defend (for containers) + +This plugin currently only exists to provide custom fleet policy UX for a set of new BPF LSM features. The first feature being container "drift prevention". + +Drift prevention is a way to block when executables are created or modified. Our agent service detects these events, and applies a set of selectors and responses configured to either block, alert or both. + +## Example configuration +``` +selectors: + # default selector (user can modify or remove if they want) + - name: default + operation: [createExecutable, modifyExecutable, execMemFd] + + # example custom selector + - name: nginxOnly + containerImageName: + - nginx + + # example selector used for exclude + - name: excludeCustomNginxBuild + containerImageTag: + - staging + +# responses are evaluated from top to bottom +# only the first response with a match will run its actions +responses: + - match: [nginxOnly] + exclude: [excludeCustomNginxBuild] + actions: [alert, block] + + # default response + # delete this if no default response needed + - match: [default] + actions: [alert] +``` + +--- + +## Development + +## pre commit checks + +``` +node scripts/type_check.js --project x-pack/plugins/cloud_defend/tsconfig.json +yarn test:jest x-pack/plugins/cloud_defend +``` + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment. diff --git a/x-pack/plugins/cloud_defend/common/constants.ts b/x-pack/plugins/cloud_defend/common/constants.ts new file mode 100755 index 0000000000000..860f3d4adffea --- /dev/null +++ b/x-pack/plugins/cloud_defend/common/constants.ts @@ -0,0 +1,12 @@ +/* + * 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 PLUGIN_ID = 'cloudDefend'; +export const PLUGIN_NAME = 'cloudDefend'; +export const INTEGRATION_PACKAGE_NAME = 'cloud_defend'; +export const INPUT_CONTROL = 'cloud_defend/control'; +export const ALERTS_DATASET = 'cloud_defend.alerts'; diff --git a/x-pack/plugins/cloud_defend/jest.config.js b/x-pack/plugins/cloud_defend/jest.config.js new file mode 100644 index 0000000000000..144b2f1ad9e19 --- /dev/null +++ b/x-pack/plugins/cloud_defend/jest.config.js @@ -0,0 +1,18 @@ +/* + * 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. + */ + +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/cloud_defend'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/cloud_defend', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/cloud_defend/{common,public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/x-pack/plugins/cloud_defend/kibana.json b/x-pack/plugins/cloud_defend/kibana.json new file mode 100755 index 0000000000000..af91d3eec34a0 --- /dev/null +++ b/x-pack/plugins/cloud_defend/kibana.json @@ -0,0 +1,14 @@ +{ + "id": "cloudDefend", + "version": "1.0.0", + "kibanaVersion": "kibana", + "owner": { + "name": "Cloud Native Integrations", + "githubTeam": "sec-cloudnative-integrations" + }, + "description": "Defend for Containers", + "server": false, + "ui": true, + "requiredPlugins": ["fleet", "kibanaReact"], + "optionalPlugins": [] +} diff --git a/x-pack/plugins/cloud_defend/public/common/utils.ts b/x-pack/plugins/cloud_defend/public/common/utils.ts new file mode 100644 index 0000000000000..7f2ee09ad3e07 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/common/utils.ts @@ -0,0 +1,12 @@ +/* + * 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 { NewPackagePolicy } from '@kbn/fleet-plugin/public'; + +export function getInputFromPolicy(policy: NewPackagePolicy, inputId: string) { + return policy.inputs.find((input) => input.type === inputId); +} diff --git a/x-pack/plugins/cloud_defend/public/components/config_yaml_view/__mocks__/resizeobserver.js b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/__mocks__/resizeobserver.js new file mode 100644 index 0000000000000..489a06a024c71 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/__mocks__/resizeobserver.js @@ -0,0 +1,21 @@ +/* + * 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. + */ + +class ResizeObserver { + observe() { + // do nothing + } + unobserve() { + // do nothing + } + disconnect() { + // do nothing + } +} + +window.ResizeObserver = ResizeObserver; +export default ResizeObserver; diff --git a/x-pack/plugins/cloud_defend/public/components/config_yaml_view/__mocks__/worker.js b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/__mocks__/worker.js new file mode 100644 index 0000000000000..0db713b040c02 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/__mocks__/worker.js @@ -0,0 +1,22 @@ +/* + * 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. + */ + +class Worker { + constructor(stringUrl) { + this.url = stringUrl; + this.onmessage = () => {}; + } + + postMessage(msg) { + this.onmessage(msg); + } + + terminate() {} +} + +window.Worker = Worker; +export default Worker; diff --git a/x-pack/plugins/cloud_defend/public/components/config_yaml_view/hooks/use_config_model.ts b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/hooks/use_config_model.ts new file mode 100644 index 0000000000000..58d9a8c9a46c2 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/hooks/use_config_model.ts @@ -0,0 +1,150 @@ +/* + * 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 { useMemo } from 'react'; +import yaml from 'js-yaml'; +import { setDiagnosticsOptions } from 'monaco-yaml'; +import { monaco } from '@kbn/monaco'; + +const { Uri, editor } = monaco; + +const SCHEMA_URI = 'http://elastic.co/cloud_defend.yaml'; +const modelUri = Uri.parse(SCHEMA_URI); + +export const useConfigModel = (configuration: string) => { + const json = useMemo(() => { + try { + return yaml.load(configuration); + } catch { + return { selectors: [] }; + } + }, [configuration]); + + return useMemo(() => { + const selectorNames = json?.selectors?.map((selector: any) => selector.name) || []; + + setDiagnosticsOptions({ + validate: true, + completion: true, + hover: true, + schemas: [ + { + uri: SCHEMA_URI, + fileMatch: [String(modelUri)], + schema: { + type: 'object', + required: ['selectors', 'responses'], + additionalProperties: false, + properties: { + selectors: { + type: 'array', + minItems: 1, + items: { $ref: '#/$defs/selector' }, + }, + responses: { + type: 'array', + minItems: 1, + items: { $ref: '#/$defs/response' }, + }, + }, + $defs: { + selector: { + type: 'object', + required: ['name'], + additionalProperties: false, + anyOf: [ + { required: ['operation'] }, + { required: ['containerImageName'] }, + { required: ['containerImageTag'] }, + { required: ['targetFilePath'] }, + { required: ['orchestratorClusterId'] }, + { required: ['orchestratorClusterName'] }, + { required: ['orchestratorNamespace'] }, + { required: ['orchestratorResourceLabel'] }, + { required: ['orchestratorResourceName'] }, + { required: ['orchestratorType'] }, + ], + properties: { + name: { + type: 'string', + }, + operation: { + type: 'array', + minItems: 1, + items: { enum: ['createExecutable', 'modifyExecutable', 'execMemFd'] }, + }, + containerImageName: { + type: 'array', + minItems: 1, + items: { type: 'string' }, + }, + containerImageTag: { + type: 'array', + minItems: 1, + items: { type: 'string' }, + }, + targetFilePath: { + type: 'array', + minItems: 1, + items: { type: 'string' }, + }, + orchestratorClusterId: { + type: 'array', + minItems: 1, + items: { type: 'string' }, + }, + orchestratorClusterName: { + type: 'array', + minItems: 1, + items: { type: 'string' }, + }, + orchestratorNamespace: { + type: 'array', + minItems: 1, + items: { type: 'string' }, + }, + orchestratorResourceLabel: { + type: 'array', + minItems: 1, + items: { type: 'string' }, + }, + orchestratorResourceName: { + type: 'array', + minItems: 1, + items: { type: 'string' }, + }, + orchestratorType: { + type: 'array', + minItems: 1, + items: { enum: ['kubernetes'] }, + }, + }, + }, + response: { + type: 'object', + required: ['match', 'actions'], + additionalProperties: false, + properties: { + match: { type: 'array', minItems: 1, items: { enum: selectorNames } }, + exclude: { type: 'array', items: { enum: selectorNames } }, + actions: { type: 'array', minItems: 1, items: { enum: ['alert', 'block'] } }, + }, + }, + }, + }, + }, + ], + }); + + let model = editor.getModel(modelUri); + + if (model === null) { + model = editor.createModel(configuration, 'yaml', modelUri); + } + + return model; + }, [configuration, json.selectors]); +}; diff --git a/x-pack/plugins/cloud_defend/public/components/config_yaml_view/index.test.tsx b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/index.test.tsx new file mode 100644 index 0000000000000..78429546b31f3 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/index.test.tsx @@ -0,0 +1,62 @@ +/* + * 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 from 'react'; +import { render } from '@testing-library/react'; +import '@kbn/kibana-react-plugin/public/code_editor/code_editor.test.helpers'; +import { TestProvider } from '../../test/test_provider'; +import { getCloudDefendNewPolicyMock } from './mocks'; +import { ConfigYamlView } from '.'; +import './__mocks__/worker'; +import './__mocks__/resizeobserver'; + +// @ts-ignore-next +window.Worker = Worker; + +describe('', () => { + beforeAll(() => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); + }); + + const onChange = jest.fn(); + + const WrappedComponent = ({ policy = getCloudDefendNewPolicyMock() }) => { + return ( + + ; + + ); + }; + + beforeEach(() => { + onChange.mockClear(); + }); + + it('renders a checkbox to toggle BPF/LSM control mechanism', () => { + const { getByTestId } = render(); + const input = getByTestId('cloud-defend-control-toggle') as HTMLInputElement; + expect(input).toBeInTheDocument(); + expect(input).toBeEnabled(); + }); + + it('renders a yaml editor', () => { + const { getByTestId } = render(); + const el = getByTestId('monacoEditorTextarea') as HTMLTextAreaElement; + expect(el).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cloud_defend/public/components/config_yaml_view/index.tsx b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/index.tsx new file mode 100644 index 0000000000000..21bee7282b17a --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/index.tsx @@ -0,0 +1,124 @@ +/* + * 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, useEffect, useState } from 'react'; +import { EuiSwitch, EuiSpacer, EuiText, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { CodeEditor, YamlLang } from '@kbn/kibana-react-plugin/public'; +import { NewPackagePolicy } from '@kbn/fleet-plugin/public'; +import { monaco } from '@kbn/monaco'; +import { INPUT_CONTROL } from '../../../common/constants'; +import { useStyles } from './styles'; +import { useConfigModel } from './hooks/use_config_model'; +import { getInputFromPolicy } from '../../common/utils'; +import * as i18n from './translations'; + +const { editor } = monaco; + +interface OnChangeDeps { + isValid: boolean; + updatedPolicy: NewPackagePolicy; +} + +interface ConfigYamlViewDeps { + policy: NewPackagePolicy; + onChange(opts: OnChangeDeps): void; +} + +interface ConfigError { + line: number; + message: string; +} + +export const ConfigYamlView = ({ policy, onChange }: ConfigYamlViewDeps) => { + const styles = useStyles(); + const [errors, setErrors] = useState([]); + const input = getInputFromPolicy(policy, INPUT_CONTROL); + const configuration = input?.vars?.configuration?.value || ''; + const currentModel = useConfigModel(configuration); + const controlEnabled = !!input?.enabled; + + useEffect(() => { + const listener = editor.onDidChangeMarkers(([resource]) => { + const markers = editor.getModelMarkers({ resource }); + const errs = markers.map((marker) => { + const error: ConfigError = { + line: marker.startLineNumber, + message: marker.message, + }; + + return error; + }); + + onChange({ isValid: errs.length === 0, updatedPolicy: policy }); + setErrors(errs); + }); + + return () => { + listener.dispose(); + }; + }, [onChange, policy]); + + const onYamlChange = useCallback( + (value) => { + if (input?.vars) { + input.vars.configuration.value = value; + onChange({ isValid: errors.length === 0, updatedPolicy: policy }); + } + }, + [errors.length, input, onChange, policy] + ); + + const onToggleEnabled = useCallback( + (e) => { + if (input) { + input.enabled = e.target.checked; + onChange({ isValid: errors.length === 0, updatedPolicy: policy }); + } + }, + [errors.length, input, onChange, policy] + ); + + return ( + + + + + + {i18n.enableControlHelp} + + + {controlEnabled && ( + + +

    {i18n.controlYaml}

    +
    + + + {i18n.controlYamlHelp} + + +
    + +
    + +
    + )} +
    + ); +}; diff --git a/x-pack/plugins/cloud_defend/public/components/config_yaml_view/mocks.ts b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/mocks.ts new file mode 100644 index 0000000000000..7e1d30d6dba22 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/mocks.ts @@ -0,0 +1,116 @@ +/* + * 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 type { NewPackagePolicy } from '@kbn/fleet-plugin/public'; +import type { PackagePolicy } from '@kbn/fleet-plugin/common'; +import { INTEGRATION_PACKAGE_NAME, INPUT_CONTROL, ALERTS_DATASET } from '../../../common/constants'; + +const MOCK_YAML_CONFIGURATION = ` +selectors: + # default selector (user can modify or remove if they want) + - name: default + operation: [createExecutable, modifyExecutable, execMemFd] + + # example custom selector + - name: nginxOnly + containerImageName: + - nginx + + # example selector used for exclude + - name: excludeCustomNginxBuild + containerImageTag: + - staging + +# responses are evaluated from top to bottom +# only the first response with a match will run its actions +responses: + - match: [nginxOnly] + exclude: [excludeCustomNginxBuild] + actions: [alert, block] + + # default response + # delete this if no default response needed + - match: [default] + actions: [alert] +`; + +export const getCloudDefendNewPolicyMock = (): NewPackagePolicy => ({ + name: 'some-cloud_defend-policy', + description: '', + namespace: 'default', + policy_id: '', + enabled: true, + inputs: [ + { + type: INPUT_CONTROL, + policy_template: INTEGRATION_PACKAGE_NAME, + enabled: true, + vars: { + configuration: { + type: 'yaml', + value: MOCK_YAML_CONFIGURATION, + }, + }, + streams: [ + { + enabled: true, + data_stream: { + type: 'logs', + dataset: ALERTS_DATASET, + }, + }, + ], + }, + ], + package: { + name: 'cloud_defend', + title: 'Kubernetes Security Posture Management', + version: '0.0.21', + }, +}); + +export const getCloudDefendPolicyMock = (): PackagePolicy => ({ + id: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1', + version: 'abcd', + revision: 1, + updated_at: '2020-06-25T16:03:38.159292', + updated_by: 'kibana', + created_at: '2020-06-25T16:03:38.159292', + created_by: 'kibana', + name: 'some-cloud_defend-policy', + description: '', + namespace: 'default', + policy_id: '', + enabled: true, + inputs: [ + { + type: INPUT_CONTROL, + policy_template: INTEGRATION_PACKAGE_NAME, + enabled: true, + vars: { + configuration: { + type: 'yaml', + value: MOCK_YAML_CONFIGURATION, + }, + }, + streams: [ + { + id: '1234', + enabled: true, + data_stream: { + type: 'logs', + dataset: ALERTS_DATASET, + }, + }, + ], + }, + ], + package: { + name: 'cloud_defend', + title: 'Kubernetes Security Posture Management', + version: '0.0.21', + }, +}); diff --git a/x-pack/plugins/cloud_defend/public/components/config_yaml_view/styles.ts b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/styles.ts new file mode 100644 index 0000000000000..0d49630b95d66 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/styles.ts @@ -0,0 +1,19 @@ +/* + * 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 { useMemo } from 'react'; +import { CSSObject } from '@emotion/react'; + +export const useStyles = () => { + return useMemo(() => { + const yamlEditor: CSSObject = { + height: '400px', + }; + + return { yamlEditor }; + }, []); +}; diff --git a/x-pack/plugins/cloud_defend/public/components/config_yaml_view/translations.ts b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/translations.ts new file mode 100644 index 0000000000000..f7765910d38a5 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/config_yaml_view/translations.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const enableControl = i18n.translate('xpack.cloudDefend.enableControl', { + defaultMessage: 'Enable BPF/LSM controls', +}); + +export const enableControlHelp = i18n.translate('xpack.cloudDefend.enableControlHelp', { + defaultMessage: + 'Enables BPF/LSM control mechanism, for use with FIM and container drift prevention.', +}); + +export const controlYaml = i18n.translate('xpack.cloudDefend.controlYaml', { + defaultMessage: 'Configuration yaml', +}); + +export const controlYamlHelp = i18n.translate('xpack.cloudDefend.controlYamlHelp', { + defaultMessage: + 'Configure BPF/LSM controls by creating selectors, and responses below. To learn more click ', +}); diff --git a/x-pack/plugins/cloud_defend/public/components/fleet_extensions/policy_extension_create.tsx b/x-pack/plugins/cloud_defend/public/components/fleet_extensions/policy_extension_create.tsx new file mode 100644 index 0000000000000..4d46204d40eff --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/fleet_extensions/policy_extension_create.tsx @@ -0,0 +1,20 @@ +/* + * 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, { memo } from 'react'; +import type { PackagePolicyCreateExtensionComponentProps } from '@kbn/fleet-plugin/public'; +import { ConfigYamlView } from '../config_yaml_view'; + +export const CloudDefendCreatePolicyExtension = memo( + ({ newPolicy, onChange }) => { + return ; + } +); + +CloudDefendCreatePolicyExtension.displayName = 'CloudDefendCreatePolicyExtension'; + +// eslint-disable-next-line import/no-default-export +export { CloudDefendCreatePolicyExtension as default }; diff --git a/x-pack/plugins/cloud_defend/public/components/fleet_extensions/policy_extension_edit.tsx b/x-pack/plugins/cloud_defend/public/components/fleet_extensions/policy_extension_edit.tsx new file mode 100644 index 0000000000000..c1e1b6c755d53 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/fleet_extensions/policy_extension_edit.tsx @@ -0,0 +1,20 @@ +/* + * 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, { memo } from 'react'; +import type { PackagePolicyEditExtensionComponentProps } from '@kbn/fleet-plugin/public'; +import { ConfigYamlView } from '../config_yaml_view'; + +export const CloudDefendEditPolicyExtension = memo( + ({ newPolicy, onChange }) => { + return ; + } +); + +CloudDefendEditPolicyExtension.displayName = 'CloudDefendEditPolicyExtension'; + +// eslint-disable-next-line import/no-default-export +export { CloudDefendEditPolicyExtension as default }; diff --git a/x-pack/plugins/cloud_defend/public/index.ts b/x-pack/plugins/cloud_defend/public/index.ts new file mode 100755 index 0000000000000..fd8099aa2ed11 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { CloudDefendPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin() { + return new CloudDefendPlugin(); +} +export type { CloudDefendPluginSetup, CloudDefendPluginStart } from './types'; diff --git a/x-pack/plugins/cloud_defend/public/plugin.ts b/x-pack/plugins/cloud_defend/public/plugin.ts new file mode 100755 index 0000000000000..5bbb1215e2270 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/plugin.ts @@ -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 { lazy } from 'react'; +import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import { + CloudDefendPluginSetup, + CloudDefendPluginStart, + CloudDefendPluginStartDeps, +} from './types'; +import { INTEGRATION_PACKAGE_NAME } from '../common/constants'; + +const LazyEditPolicy = lazy(() => import('./components/fleet_extensions/policy_extension_edit')); +const LazyCreatePolicy = lazy( + () => import('./components/fleet_extensions/policy_extension_create') +); + +export class CloudDefendPlugin implements Plugin { + public setup(core: CoreSetup): CloudDefendPluginSetup { + // Return methods that should be available to other plugins + return {}; + } + + public start(core: CoreStart, plugins: CloudDefendPluginStartDeps): CloudDefendPluginStart { + plugins.fleet.registerExtension({ + package: INTEGRATION_PACKAGE_NAME, + view: 'package-policy-create', + Component: LazyCreatePolicy, + }); + + plugins.fleet.registerExtension({ + package: INTEGRATION_PACKAGE_NAME, + view: 'package-policy-edit', + Component: LazyEditPolicy, + }); + + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/cloud_defend/public/test/test_provider.tsx b/x-pack/plugins/cloud_defend/public/test/test_provider.tsx new file mode 100755 index 0000000000000..8f692a7f381ec --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/test/test_provider.tsx @@ -0,0 +1,48 @@ +/* + * 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 type { AppMountParameters, CoreStart } from '@kbn/core/public'; +import React, { useMemo } from 'react'; +import { I18nProvider } from '@kbn/i18n-react'; +import { Router, Switch, Route } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { coreMock } from '@kbn/core/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { fleetMock } from '@kbn/fleet-plugin/public/mocks'; +import type { CloudDefendPluginStartDeps } from '../types'; + +interface CspAppDeps { + core: CoreStart; + deps: CloudDefendPluginStartDeps; + params: AppMountParameters; +} + +export const TestProvider: React.FC> = ({ + core = coreMock.createStart(), + deps = { + data: dataPluginMock.createStartContract(), + fleet: fleetMock.createStartMock(), + }, + params = coreMock.createAppMountParameters(), + children, +} = {}) => { + const queryClient = useMemo(() => new QueryClient(), []); + return ( + + + + + + <>{children}} /> + + + + + + ); +}; diff --git a/x-pack/plugins/cloud_defend/public/test/utils.ts b/x-pack/plugins/cloud_defend/public/test/utils.ts new file mode 100644 index 0000000000000..c523c102a7bd5 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/test/utils.ts @@ -0,0 +1,17 @@ +/* + * 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 { screen } from '@testing-library/react'; + +export const expectIdsInDoc = ({ be = [], notToBe = [] }: { be: string[]; notToBe?: string[] }) => { + be.forEach((testId) => { + expect(screen.getByTestId(testId)).toBeInTheDocument(); + }); + notToBe.forEach((testId) => { + expect(screen.queryByTestId(testId)).not.toBeInTheDocument(); + }); +}; diff --git a/x-pack/plugins/cloud_defend/public/types.ts b/x-pack/plugins/cloud_defend/public/types.ts new file mode 100755 index 0000000000000..afdc76cb852c9 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/types.ts @@ -0,0 +1,20 @@ +/* + * 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 type { FleetSetup, FleetStart } from '@kbn/fleet-plugin/public'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface CloudDefendPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface CloudDefendPluginStart {} + +export interface CloudDefendPluginSetupDeps { + fleet: FleetSetup; +} +export interface CloudDefendPluginStartDeps { + fleet: FleetStart; +} diff --git a/x-pack/plugins/cloud_defend/tsconfig.json b/x-pack/plugins/cloud_defend/tsconfig.json new file mode 100755 index 0000000000000..e40b3d8e9c273 --- /dev/null +++ b/x-pack/plugins/cloud_defend/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true + }, + "include": [ + "common/**/*", + "public/**/*", + "../../../typings/**/*" + ], + "kbn_references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../x-pack/plugins/fleet/tsconfig.json" } + ] +} diff --git a/yarn.lock b/yarn.lock index 8c63ce46798f7..d7a0ce8f49943 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7018,7 +7018,7 @@ "@types/tough-cookie" "*" parse5 "^7.0.0" -"@types/json-schema@*", "@types/json-schema@^7", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7", "@types/json-schema@^7.0.0", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== @@ -17962,7 +17962,7 @@ js-yaml@3.14.1, js-yaml@^3.13.1, js-yaml@^3.14.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@4.1.0, js-yaml@^4.1.0: +js-yaml@4.1.0, js-yaml@^4.0.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== @@ -19911,6 +19911,19 @@ monaco-editor@*, monaco-editor@^0.24.0: resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.24.0.tgz#990b55096bcc95d08d8d28e55264c6eb17707269" integrity sha512-o1f0Lz6ABFNTtnEqqqvlY9qzNx24rQZx1RgYNQ8SkWkE+Ka63keHH/RqxQ4QhN4fs/UYOnvAtEUZsPrzccH++A== +monaco-yaml@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/monaco-yaml/-/monaco-yaml-3.2.1.tgz#45ce9f7f8140dc26249ac99eb0a9e5a9ab9beb87" + integrity sha512-2geAd5I7H1SMgwTHBuyPAPK9WTAzxbl9XwDl5h6NY6n9j4qnlLLQKK1i0P9cAmUiV2uaiViz69RLNWqVU5BVsg== + dependencies: + "@types/json-schema" "^7.0.0" + js-yaml "^4.0.0" + path-browserify "^1.0.0" + prettier "2.0.5" + vscode-languageserver-textdocument "^1.0.0" + vscode-languageserver-types "^3.0.0" + yaml-language-server-parser "^0.1.0" + monitor-event-loop-delay@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/monitor-event-loop-delay/-/monitor-event-loop-delay-1.0.0.tgz#b5ab78165a3bb93f2b275c50d01430c7f155d1f7" @@ -21217,7 +21230,7 @@ path-browserify@0.0.1: resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== -path-browserify@^1.0.1: +path-browserify@^1.0.0, path-browserify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== @@ -21952,6 +21965,11 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" +prettier@2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" + integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== + "prettier@>=2.2.1 <=2.3.0": version "2.2.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" @@ -27544,6 +27562,16 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vscode-languageserver-textdocument@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.7.tgz#16df468d5c2606103c90554ae05f9f3d335b771b" + integrity sha512-bFJH7UQxlXT8kKeyiyu41r22jCZXG8kuuVVA33OEJn1diWOZK5n8zBSPZFHVBOu8kXZ6h0LIRhf5UnCo61J4Hg== + +vscode-languageserver-types@^3.0.0: + version "3.17.2" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz#b2c2e7de405ad3d73a883e91989b850170ffc4f2" + integrity sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA== + vt-pbf@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.3.tgz#68fd150756465e2edae1cc5c048e063916dcfaac" @@ -28318,6 +28346,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml-language-server-parser@^0.1.0: + version "0.1.3" + resolved "https://registry.yarnpkg.com/yaml-language-server-parser/-/yaml-language-server-parser-0.1.3.tgz#f0e9082068291c7c330eefa1f3c9f1b4c3c54183" + integrity sha512-xD2I+6M/vqQvcy4ded8JpXUaDHXmZMdhIO3OpuiFxstutwnW4whrfDzNcrsfXVdgMWqOUpdv3747Q081PFN1+g== + yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" From dd3efeaa20493f8777599b714d1288b5fd57ae95 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 20 Dec 2022 00:46:09 -0500 Subject: [PATCH 35/55] [api-docs] 2022-12-20 Daily api_docs build (#147826) Generated by https://buildkite.com/elastic/kibana-api-docs-daily/builds/192 --- api_docs/actions.mdx | 2 +- api_docs/advanced_settings.mdx | 2 +- api_docs/aiops.mdx | 2 +- api_docs/alerting.devdocs.json | 14 + api_docs/alerting.mdx | 4 +- api_docs/apm.mdx | 2 +- api_docs/banners.mdx | 2 +- api_docs/bfetch.mdx | 2 +- api_docs/canvas.mdx | 2 +- api_docs/cases.mdx | 2 +- api_docs/charts.mdx | 2 +- api_docs/cloud.mdx | 2 +- api_docs/cloud_chat.mdx | 2 +- api_docs/cloud_data_migration.mdx | 2 +- api_docs/cloud_defend.devdocs.json | 55 ++ api_docs/cloud_defend.mdx | 33 ++ api_docs/cloud_experiments.mdx | 2 +- api_docs/cloud_security_posture.mdx | 2 +- api_docs/console.mdx | 2 +- api_docs/controls.devdocs.json | 135 +++-- api_docs/controls.mdx | 4 +- api_docs/core.devdocs.json | 264 +++++---- api_docs/core.mdx | 4 +- api_docs/custom_integrations.mdx | 2 +- api_docs/dashboard.mdx | 2 +- api_docs/dashboard_enhanced.mdx | 2 +- api_docs/data.devdocs.json | 40 +- api_docs/data.mdx | 2 +- api_docs/data_query.devdocs.json | 61 +-- api_docs/data_query.mdx | 2 +- api_docs/data_search.mdx | 2 +- api_docs/data_view_editor.mdx | 2 +- api_docs/data_view_field_editor.mdx | 2 +- api_docs/data_view_management.mdx | 2 +- api_docs/data_views.devdocs.json | 60 ++- api_docs/data_views.mdx | 2 +- api_docs/data_visualizer.mdx | 2 +- api_docs/deprecations_by_api.mdx | 2 +- api_docs/deprecations_by_plugin.mdx | 8 +- api_docs/deprecations_by_team.mdx | 2 +- api_docs/dev_tools.mdx | 2 +- api_docs/discover.mdx | 2 +- api_docs/discover_enhanced.mdx | 2 +- api_docs/embeddable.devdocs.json | 36 +- api_docs/embeddable.mdx | 4 +- api_docs/embeddable_enhanced.mdx | 2 +- api_docs/encrypted_saved_objects.mdx | 2 +- api_docs/enterprise_search.mdx | 2 +- api_docs/es_ui_shared.mdx | 2 +- api_docs/event_annotation.mdx | 2 +- api_docs/event_log.mdx | 2 +- api_docs/expression_error.mdx | 2 +- api_docs/expression_gauge.mdx | 2 +- api_docs/expression_heatmap.mdx | 2 +- api_docs/expression_image.mdx | 2 +- api_docs/expression_legacy_metric_vis.mdx | 2 +- api_docs/expression_metric.mdx | 2 +- api_docs/expression_metric_vis.mdx | 2 +- api_docs/expression_partition_vis.mdx | 2 +- api_docs/expression_repeat_image.mdx | 2 +- api_docs/expression_reveal_image.mdx | 2 +- api_docs/expression_shape.mdx | 2 +- api_docs/expression_tagcloud.mdx | 2 +- api_docs/expression_x_y.mdx | 2 +- api_docs/expressions.mdx | 2 +- api_docs/features.mdx | 2 +- api_docs/field_formats.mdx | 2 +- api_docs/file_upload.mdx | 2 +- api_docs/files.devdocs.json | 4 + api_docs/files.mdx | 2 +- api_docs/files_management.mdx | 2 +- api_docs/fleet.mdx | 2 +- api_docs/global_search.mdx | 2 +- api_docs/guided_onboarding.mdx | 2 +- api_docs/home.mdx | 2 +- api_docs/index_lifecycle_management.mdx | 2 +- api_docs/index_management.mdx | 2 +- api_docs/infra.mdx | 2 +- api_docs/inspector.mdx | 2 +- api_docs/interactive_setup.mdx | 2 +- api_docs/kbn_ace.mdx | 2 +- api_docs/kbn_aiops_components.mdx | 2 +- api_docs/kbn_aiops_utils.mdx | 2 +- api_docs/kbn_alerts.mdx | 2 +- api_docs/kbn_analytics.mdx | 2 +- api_docs/kbn_analytics_client.mdx | 2 +- ..._analytics_shippers_elastic_v3_browser.mdx | 2 +- ...n_analytics_shippers_elastic_v3_common.mdx | 2 +- ...n_analytics_shippers_elastic_v3_server.mdx | 2 +- api_docs/kbn_analytics_shippers_fullstory.mdx | 2 +- api_docs/kbn_analytics_shippers_gainsight.mdx | 2 +- api_docs/kbn_apm_config_loader.mdx | 2 +- api_docs/kbn_apm_synthtrace.mdx | 2 +- api_docs/kbn_apm_utils.mdx | 2 +- api_docs/kbn_axe_config.mdx | 2 +- api_docs/kbn_cases_components.mdx | 2 +- api_docs/kbn_chart_icons.mdx | 2 +- api_docs/kbn_ci_stats_core.mdx | 2 +- api_docs/kbn_ci_stats_performance_metrics.mdx | 2 +- api_docs/kbn_ci_stats_reporter.mdx | 2 +- api_docs/kbn_cli_dev_mode.mdx | 2 +- api_docs/kbn_coloring.mdx | 2 +- api_docs/kbn_config.mdx | 2 +- api_docs/kbn_config_mocks.mdx | 2 +- api_docs/kbn_config_schema.mdx | 2 +- .../kbn_content_management_content_editor.mdx | 2 +- ...content_management_table_list.devdocs.json | 6 +- .../kbn_content_management_table_list.mdx | 2 +- api_docs/kbn_core_analytics_browser.mdx | 2 +- .../kbn_core_analytics_browser_internal.mdx | 2 +- api_docs/kbn_core_analytics_browser_mocks.mdx | 2 +- api_docs/kbn_core_analytics_server.mdx | 2 +- .../kbn_core_analytics_server_internal.mdx | 2 +- api_docs/kbn_core_analytics_server_mocks.mdx | 2 +- api_docs/kbn_core_application_browser.mdx | 2 +- .../kbn_core_application_browser_internal.mdx | 2 +- .../kbn_core_application_browser_mocks.mdx | 2 +- api_docs/kbn_core_application_common.mdx | 2 +- api_docs/kbn_core_apps_browser_internal.mdx | 2 +- api_docs/kbn_core_apps_browser_mocks.mdx | 2 +- api_docs/kbn_core_apps_server_internal.mdx | 2 +- api_docs/kbn_core_base_browser_mocks.mdx | 2 +- api_docs/kbn_core_base_common.mdx | 2 +- api_docs/kbn_core_base_server_internal.mdx | 2 +- api_docs/kbn_core_base_server_mocks.mdx | 2 +- .../kbn_core_capabilities_browser_mocks.mdx | 2 +- api_docs/kbn_core_capabilities_common.mdx | 2 +- api_docs/kbn_core_capabilities_server.mdx | 2 +- .../kbn_core_capabilities_server_mocks.mdx | 2 +- api_docs/kbn_core_chrome_browser.mdx | 2 +- api_docs/kbn_core_chrome_browser_mocks.mdx | 2 +- api_docs/kbn_core_config_server_internal.mdx | 2 +- api_docs/kbn_core_deprecations_browser.mdx | 2 +- ...kbn_core_deprecations_browser_internal.mdx | 2 +- .../kbn_core_deprecations_browser_mocks.mdx | 2 +- api_docs/kbn_core_deprecations_common.mdx | 2 +- api_docs/kbn_core_deprecations_server.mdx | 2 +- .../kbn_core_deprecations_server_internal.mdx | 2 +- .../kbn_core_deprecations_server_mocks.mdx | 2 +- api_docs/kbn_core_doc_links_browser.mdx | 2 +- api_docs/kbn_core_doc_links_browser_mocks.mdx | 2 +- api_docs/kbn_core_doc_links_server.mdx | 2 +- api_docs/kbn_core_doc_links_server_mocks.mdx | 2 +- ...e_elasticsearch_client_server_internal.mdx | 2 +- ...core_elasticsearch_client_server_mocks.mdx | 2 +- api_docs/kbn_core_elasticsearch_server.mdx | 2 +- ...kbn_core_elasticsearch_server_internal.mdx | 2 +- .../kbn_core_elasticsearch_server_mocks.mdx | 2 +- .../kbn_core_environment_server_internal.mdx | 2 +- .../kbn_core_environment_server_mocks.mdx | 2 +- .../kbn_core_execution_context_browser.mdx | 2 +- ...ore_execution_context_browser_internal.mdx | 2 +- ...n_core_execution_context_browser_mocks.mdx | 2 +- .../kbn_core_execution_context_common.mdx | 2 +- .../kbn_core_execution_context_server.mdx | 2 +- ...core_execution_context_server_internal.mdx | 2 +- ...bn_core_execution_context_server_mocks.mdx | 2 +- api_docs/kbn_core_fatal_errors_browser.mdx | 2 +- .../kbn_core_fatal_errors_browser_mocks.mdx | 2 +- api_docs/kbn_core_http_browser.mdx | 2 +- api_docs/kbn_core_http_browser_internal.mdx | 2 +- api_docs/kbn_core_http_browser_mocks.mdx | 2 +- api_docs/kbn_core_http_common.mdx | 2 +- .../kbn_core_http_context_server_mocks.mdx | 2 +- ...re_http_request_handler_context_server.mdx | 2 +- api_docs/kbn_core_http_resources_server.mdx | 2 +- ...bn_core_http_resources_server_internal.mdx | 2 +- .../kbn_core_http_resources_server_mocks.mdx | 2 +- .../kbn_core_http_router_server_internal.mdx | 2 +- .../kbn_core_http_router_server_mocks.mdx | 2 +- api_docs/kbn_core_http_server.devdocs.json | 16 - api_docs/kbn_core_http_server.mdx | 4 +- api_docs/kbn_core_http_server_internal.mdx | 2 +- api_docs/kbn_core_http_server_mocks.mdx | 2 +- api_docs/kbn_core_i18n_browser.mdx | 2 +- api_docs/kbn_core_i18n_browser_mocks.mdx | 2 +- api_docs/kbn_core_i18n_server.mdx | 2 +- api_docs/kbn_core_i18n_server_internal.mdx | 2 +- api_docs/kbn_core_i18n_server_mocks.mdx | 2 +- .../kbn_core_injected_metadata_browser.mdx | 2 +- ...n_core_injected_metadata_browser_mocks.mdx | 2 +- ...kbn_core_integrations_browser_internal.mdx | 2 +- .../kbn_core_integrations_browser_mocks.mdx | 2 +- api_docs/kbn_core_lifecycle_browser.mdx | 2 +- api_docs/kbn_core_lifecycle_browser_mocks.mdx | 2 +- api_docs/kbn_core_lifecycle_server.mdx | 2 +- api_docs/kbn_core_lifecycle_server_mocks.mdx | 2 +- api_docs/kbn_core_logging_browser_mocks.mdx | 2 +- api_docs/kbn_core_logging_common_internal.mdx | 2 +- api_docs/kbn_core_logging_server.mdx | 2 +- api_docs/kbn_core_logging_server_internal.mdx | 2 +- api_docs/kbn_core_logging_server_mocks.mdx | 2 +- ...ore_metrics_collectors_server_internal.mdx | 2 +- ...n_core_metrics_collectors_server_mocks.mdx | 2 +- api_docs/kbn_core_metrics_server.mdx | 2 +- api_docs/kbn_core_metrics_server_internal.mdx | 2 +- api_docs/kbn_core_metrics_server_mocks.mdx | 2 +- api_docs/kbn_core_mount_utils_browser.mdx | 2 +- api_docs/kbn_core_node_server.mdx | 2 +- api_docs/kbn_core_node_server_internal.mdx | 2 +- api_docs/kbn_core_node_server_mocks.mdx | 2 +- api_docs/kbn_core_notifications_browser.mdx | 2 +- ...bn_core_notifications_browser_internal.mdx | 2 +- .../kbn_core_notifications_browser_mocks.mdx | 2 +- api_docs/kbn_core_overlays_browser.mdx | 2 +- .../kbn_core_overlays_browser_internal.mdx | 2 +- api_docs/kbn_core_overlays_browser_mocks.mdx | 2 +- api_docs/kbn_core_plugins_browser.mdx | 2 +- api_docs/kbn_core_plugins_browser_mocks.mdx | 2 +- api_docs/kbn_core_plugins_server.mdx | 2 +- api_docs/kbn_core_plugins_server_mocks.mdx | 2 +- api_docs/kbn_core_preboot_server.mdx | 2 +- api_docs/kbn_core_preboot_server_mocks.mdx | 2 +- api_docs/kbn_core_rendering_browser_mocks.mdx | 2 +- .../kbn_core_rendering_server_internal.mdx | 2 +- api_docs/kbn_core_rendering_server_mocks.mdx | 2 +- api_docs/kbn_core_root_server_internal.mdx | 2 +- .../kbn_core_saved_objects_api_browser.mdx | 2 +- .../kbn_core_saved_objects_api_server.mdx | 2 +- ...core_saved_objects_api_server_internal.mdx | 2 +- ...bn_core_saved_objects_api_server_mocks.mdx | 2 +- ...ore_saved_objects_base_server_internal.mdx | 2 +- ...n_core_saved_objects_base_server_mocks.mdx | 2 +- api_docs/kbn_core_saved_objects_browser.mdx | 2 +- ...bn_core_saved_objects_browser_internal.mdx | 2 +- .../kbn_core_saved_objects_browser_mocks.mdx | 2 +- api_docs/kbn_core_saved_objects_common.mdx | 2 +- ..._objects_import_export_server_internal.mdx | 2 +- ...ved_objects_import_export_server_mocks.mdx | 2 +- ...aved_objects_migration_server_internal.mdx | 2 +- ...e_saved_objects_migration_server_mocks.mdx | 2 +- ...kbn_core_saved_objects_server.devdocs.json | 158 +++++- api_docs/kbn_core_saved_objects_server.mdx | 4 +- ...kbn_core_saved_objects_server_internal.mdx | 2 +- .../kbn_core_saved_objects_server_mocks.mdx | 2 +- ...re_saved_objects_utils_server.devdocs.json | 168 ++++++ .../kbn_core_saved_objects_utils_server.mdx | 4 +- api_docs/kbn_core_status_common.mdx | 2 +- api_docs/kbn_core_status_common_internal.mdx | 2 +- api_docs/kbn_core_status_server.mdx | 2 +- api_docs/kbn_core_status_server_internal.mdx | 2 +- api_docs/kbn_core_status_server_mocks.mdx | 2 +- ...core_test_helpers_deprecations_getters.mdx | 2 +- ...n_core_test_helpers_http_setup_browser.mdx | 2 +- api_docs/kbn_core_test_helpers_kbn_server.mdx | 2 +- ...n_core_test_helpers_so_type_serializer.mdx | 2 +- api_docs/kbn_core_test_helpers_test_utils.mdx | 2 +- api_docs/kbn_core_theme_browser.mdx | 2 +- api_docs/kbn_core_theme_browser_internal.mdx | 2 +- api_docs/kbn_core_theme_browser_mocks.mdx | 2 +- api_docs/kbn_core_ui_settings_browser.mdx | 2 +- .../kbn_core_ui_settings_browser_internal.mdx | 2 +- .../kbn_core_ui_settings_browser_mocks.mdx | 2 +- api_docs/kbn_core_ui_settings_common.mdx | 2 +- api_docs/kbn_core_ui_settings_server.mdx | 2 +- .../kbn_core_ui_settings_server_internal.mdx | 2 +- .../kbn_core_ui_settings_server_mocks.mdx | 2 +- api_docs/kbn_core_usage_data_server.mdx | 2 +- .../kbn_core_usage_data_server_internal.mdx | 2 +- api_docs/kbn_core_usage_data_server_mocks.mdx | 2 +- api_docs/kbn_crypto.mdx | 2 +- api_docs/kbn_crypto_browser.mdx | 2 +- api_docs/kbn_datemath.mdx | 2 +- api_docs/kbn_dev_cli_errors.mdx | 2 +- api_docs/kbn_dev_cli_runner.mdx | 2 +- api_docs/kbn_dev_proc_runner.mdx | 2 +- api_docs/kbn_dev_utils.mdx | 2 +- api_docs/kbn_doc_links.mdx | 2 +- api_docs/kbn_docs_utils.mdx | 2 +- api_docs/kbn_ebt_tools.mdx | 2 +- api_docs/kbn_ecs.mdx | 2 +- api_docs/kbn_es.mdx | 2 +- api_docs/kbn_es_archiver.mdx | 2 +- api_docs/kbn_es_errors.mdx | 2 +- api_docs/kbn_es_query.devdocs.json | 508 ++++++++++++++++-- api_docs/kbn_es_query.mdx | 4 +- api_docs/kbn_es_types.mdx | 2 +- api_docs/kbn_eslint_plugin_imports.mdx | 2 +- api_docs/kbn_field_types.mdx | 2 +- api_docs/kbn_find_used_node_modules.mdx | 2 +- .../kbn_ftr_common_functional_services.mdx | 2 +- api_docs/kbn_generate.mdx | 2 +- api_docs/kbn_get_repo_files.mdx | 2 +- api_docs/kbn_guided_onboarding.mdx | 2 +- api_docs/kbn_handlebars.devdocs.json | 4 +- api_docs/kbn_handlebars.mdx | 2 +- api_docs/kbn_hapi_mocks.mdx | 2 +- api_docs/kbn_health_gateway_server.mdx | 2 +- api_docs/kbn_home_sample_data_card.mdx | 2 +- api_docs/kbn_home_sample_data_tab.mdx | 2 +- api_docs/kbn_i18n.mdx | 2 +- api_docs/kbn_i18n_react.mdx | 2 +- api_docs/kbn_import_resolver.mdx | 2 +- api_docs/kbn_interpreter.mdx | 2 +- api_docs/kbn_io_ts_utils.mdx | 2 +- api_docs/kbn_jest_serializers.mdx | 2 +- api_docs/kbn_journeys.mdx | 2 +- api_docs/kbn_kibana_manifest_schema.mdx | 2 +- .../kbn_language_documentation_popover.mdx | 2 +- api_docs/kbn_logging.mdx | 2 +- api_docs/kbn_logging_mocks.mdx | 2 +- api_docs/kbn_managed_vscode_config.mdx | 2 +- api_docs/kbn_mapbox_gl.mdx | 2 +- api_docs/kbn_ml_agg_utils.mdx | 2 +- api_docs/kbn_ml_is_populated_object.mdx | 2 +- api_docs/kbn_ml_string_hash.mdx | 2 +- api_docs/kbn_monaco.devdocs.json | 32 ++ api_docs/kbn_monaco.mdx | 4 +- api_docs/kbn_optimizer.mdx | 2 +- api_docs/kbn_optimizer_webpack_helpers.mdx | 2 +- api_docs/kbn_osquery_io_ts_types.mdx | 2 +- api_docs/kbn_peggy.mdx | 2 +- ..._performance_testing_dataset_extractor.mdx | 2 +- api_docs/kbn_plugin_generator.mdx | 2 +- api_docs/kbn_plugin_helpers.mdx | 2 +- api_docs/kbn_react_field.mdx | 2 +- api_docs/kbn_repo_source_classifier.mdx | 2 +- api_docs/kbn_rison.mdx | 2 +- api_docs/kbn_rule_data_utils.mdx | 2 +- .../kbn_securitysolution_autocomplete.mdx | 2 +- api_docs/kbn_securitysolution_es_utils.mdx | 2 +- ...ion_exception_list_components.devdocs.json | 8 +- ...ritysolution_exception_list_components.mdx | 2 +- api_docs/kbn_securitysolution_hook_utils.mdx | 2 +- ..._securitysolution_io_ts_alerting_types.mdx | 2 +- .../kbn_securitysolution_io_ts_list_types.mdx | 2 +- api_docs/kbn_securitysolution_io_ts_types.mdx | 2 +- api_docs/kbn_securitysolution_io_ts_utils.mdx | 2 +- api_docs/kbn_securitysolution_list_api.mdx | 2 +- .../kbn_securitysolution_list_constants.mdx | 2 +- api_docs/kbn_securitysolution_list_hooks.mdx | 2 +- api_docs/kbn_securitysolution_list_utils.mdx | 2 +- api_docs/kbn_securitysolution_rules.mdx | 2 +- api_docs/kbn_securitysolution_t_grid.mdx | 2 +- api_docs/kbn_securitysolution_utils.mdx | 2 +- api_docs/kbn_server_http_tools.mdx | 2 +- api_docs/kbn_server_route_repository.mdx | 2 +- api_docs/kbn_shared_svg.mdx | 2 +- api_docs/kbn_shared_ux_avatar_solution.mdx | 2 +- ...ared_ux_avatar_user_profile_components.mdx | 2 +- .../kbn_shared_ux_button_exit_full_screen.mdx | 2 +- ...hared_ux_button_exit_full_screen_mocks.mdx | 2 +- api_docs/kbn_shared_ux_button_toolbar.mdx | 2 +- api_docs/kbn_shared_ux_card_no_data.mdx | 2 +- api_docs/kbn_shared_ux_card_no_data_mocks.mdx | 2 +- api_docs/kbn_shared_ux_file_context.mdx | 2 +- api_docs/kbn_shared_ux_file_image.mdx | 2 +- api_docs/kbn_shared_ux_file_image_mocks.mdx | 2 +- api_docs/kbn_shared_ux_file_mocks.mdx | 2 +- .../kbn_shared_ux_file_picker.devdocs.json | 255 +++++++++ api_docs/kbn_shared_ux_file_picker.mdx | 33 ++ .../kbn_shared_ux_file_upload.devdocs.json | 154 ++++++ api_docs/kbn_shared_ux_file_upload.mdx | 36 ++ api_docs/kbn_shared_ux_file_util.mdx | 2 +- api_docs/kbn_shared_ux_link_redirect_app.mdx | 2 +- .../kbn_shared_ux_link_redirect_app_mocks.mdx | 2 +- api_docs/kbn_shared_ux_markdown.mdx | 2 +- api_docs/kbn_shared_ux_markdown_mocks.mdx | 2 +- .../kbn_shared_ux_page_analytics_no_data.mdx | 2 +- ...shared_ux_page_analytics_no_data_mocks.mdx | 2 +- .../kbn_shared_ux_page_kibana_no_data.mdx | 2 +- ...bn_shared_ux_page_kibana_no_data_mocks.mdx | 2 +- .../kbn_shared_ux_page_kibana_template.mdx | 2 +- ...n_shared_ux_page_kibana_template_mocks.mdx | 2 +- api_docs/kbn_shared_ux_page_no_data.mdx | 2 +- .../kbn_shared_ux_page_no_data_config.mdx | 2 +- ...bn_shared_ux_page_no_data_config_mocks.mdx | 2 +- api_docs/kbn_shared_ux_page_no_data_mocks.mdx | 2 +- api_docs/kbn_shared_ux_page_solution_nav.mdx | 2 +- .../kbn_shared_ux_prompt_no_data_views.mdx | 2 +- ...n_shared_ux_prompt_no_data_views_mocks.mdx | 2 +- api_docs/kbn_shared_ux_prompt_not_found.mdx | 2 +- api_docs/kbn_shared_ux_router.mdx | 2 +- api_docs/kbn_shared_ux_router_mocks.mdx | 2 +- api_docs/kbn_shared_ux_storybook_config.mdx | 2 +- api_docs/kbn_shared_ux_storybook_mock.mdx | 2 +- api_docs/kbn_shared_ux_utility.mdx | 2 +- api_docs/kbn_some_dev_log.mdx | 2 +- api_docs/kbn_sort_package_json.mdx | 2 +- api_docs/kbn_std.mdx | 2 +- api_docs/kbn_stdio_dev_helpers.mdx | 2 +- api_docs/kbn_storybook.mdx | 2 +- api_docs/kbn_telemetry_tools.mdx | 2 +- api_docs/kbn_test.mdx | 2 +- api_docs/kbn_test_jest_helpers.mdx | 2 +- api_docs/kbn_test_subj_selector.mdx | 2 +- api_docs/kbn_tooling_log.mdx | 2 +- api_docs/kbn_type_summarizer.mdx | 2 +- api_docs/kbn_type_summarizer_core.mdx | 2 +- api_docs/kbn_typed_react_router_config.mdx | 2 +- api_docs/kbn_ui_shared_deps_src.mdx | 2 +- api_docs/kbn_ui_theme.devdocs.json | 8 +- api_docs/kbn_ui_theme.mdx | 2 +- api_docs/kbn_user_profile_components.mdx | 2 +- api_docs/kbn_utility_types.mdx | 2 +- api_docs/kbn_utility_types_jest.mdx | 2 +- api_docs/kbn_utils.mdx | 2 +- api_docs/kbn_yarn_lock_validator.mdx | 2 +- api_docs/kibana_overview.mdx | 2 +- api_docs/kibana_react.devdocs.json | 10 +- api_docs/kibana_react.mdx | 2 +- api_docs/kibana_utils.mdx | 2 +- api_docs/kubernetes_security.mdx | 2 +- api_docs/lens.mdx | 2 +- api_docs/license_api_guard.mdx | 2 +- api_docs/license_management.mdx | 2 +- api_docs/licensing.mdx | 2 +- api_docs/lists.mdx | 2 +- api_docs/management.mdx | 2 +- api_docs/maps.mdx | 2 +- api_docs/maps_ems.mdx | 2 +- api_docs/ml.mdx | 2 +- api_docs/monitoring.mdx | 2 +- api_docs/monitoring_collection.mdx | 2 +- api_docs/navigation.mdx | 2 +- api_docs/newsfeed.mdx | 2 +- api_docs/notifications.mdx | 2 +- api_docs/observability.mdx | 2 +- api_docs/osquery.mdx | 2 +- api_docs/plugin_directory.mdx | 38 +- api_docs/presentation_util.devdocs.json | 17 + api_docs/presentation_util.mdx | 4 +- api_docs/profiling.mdx | 2 +- api_docs/remote_clusters.mdx | 2 +- api_docs/reporting.mdx | 2 +- api_docs/rollup.mdx | 2 +- api_docs/rule_registry.devdocs.json | 4 +- api_docs/rule_registry.mdx | 2 +- api_docs/runtime_fields.mdx | 2 +- api_docs/saved_objects.mdx | 2 +- api_docs/saved_objects_finder.mdx | 2 +- api_docs/saved_objects_management.mdx | 2 +- api_docs/saved_objects_tagging.mdx | 2 +- .../saved_objects_tagging_oss.devdocs.json | 37 +- api_docs/saved_objects_tagging_oss.mdx | 4 +- api_docs/saved_search.mdx | 2 +- api_docs/screenshot_mode.mdx | 2 +- api_docs/screenshotting.mdx | 2 +- api_docs/security.devdocs.json | 18 +- api_docs/security.mdx | 4 +- api_docs/security_solution.mdx | 2 +- api_docs/session_view.mdx | 2 +- api_docs/share.mdx | 2 +- api_docs/snapshot_restore.mdx | 2 +- api_docs/spaces.mdx | 2 +- api_docs/stack_alerts.mdx | 2 +- api_docs/stack_connectors.mdx | 2 +- api_docs/task_manager.mdx | 2 +- api_docs/telemetry.mdx | 2 +- api_docs/telemetry_collection_manager.mdx | 2 +- api_docs/telemetry_collection_xpack.mdx | 2 +- api_docs/telemetry_management_section.mdx | 2 +- api_docs/threat_intelligence.mdx | 2 +- api_docs/timelines.mdx | 2 +- api_docs/transform.mdx | 2 +- api_docs/triggers_actions_ui.devdocs.json | 227 +++++--- api_docs/triggers_actions_ui.mdx | 4 +- api_docs/ui_actions.mdx | 2 +- api_docs/ui_actions_enhanced.mdx | 2 +- api_docs/unified_field_list.mdx | 2 +- api_docs/unified_histogram.mdx | 2 +- api_docs/unified_search.devdocs.json | 73 ++- api_docs/unified_search.mdx | 4 +- api_docs/unified_search_autocomplete.mdx | 4 +- api_docs/url_forwarding.mdx | 2 +- api_docs/usage_collection.mdx | 2 +- api_docs/ux.mdx | 2 +- api_docs/vis_default_editor.mdx | 2 +- api_docs/vis_type_gauge.mdx | 2 +- api_docs/vis_type_heatmap.mdx | 2 +- api_docs/vis_type_pie.mdx | 2 +- api_docs/vis_type_table.mdx | 2 +- api_docs/vis_type_timelion.mdx | 2 +- api_docs/vis_type_timeseries.mdx | 2 +- api_docs/vis_type_vega.mdx | 2 +- api_docs/vis_type_vislib.mdx | 2 +- api_docs/vis_type_xy.mdx | 2 +- api_docs/visualizations.mdx | 2 +- 478 files changed, 2535 insertions(+), 907 deletions(-) create mode 100644 api_docs/cloud_defend.devdocs.json create mode 100644 api_docs/cloud_defend.mdx create mode 100644 api_docs/kbn_shared_ux_file_picker.devdocs.json create mode 100644 api_docs/kbn_shared_ux_file_picker.mdx create mode 100644 api_docs/kbn_shared_ux_file_upload.devdocs.json create mode 100644 api_docs/kbn_shared_ux_file_upload.mdx diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index 6a98b3ab929c6..98232d64efebf 100644 --- a/api_docs/actions.mdx +++ b/api_docs/actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/actions title: "actions" image: https://source.unsplash.com/400x175/?github description: API docs for the actions plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'actions'] --- import actionsObj from './actions.devdocs.json'; diff --git a/api_docs/advanced_settings.mdx b/api_docs/advanced_settings.mdx index c872e7f34a140..ff2b87959c819 100644 --- a/api_docs/advanced_settings.mdx +++ b/api_docs/advanced_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/advancedSettings title: "advancedSettings" image: https://source.unsplash.com/400x175/?github description: API docs for the advancedSettings plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'advancedSettings'] --- import advancedSettingsObj from './advanced_settings.devdocs.json'; diff --git a/api_docs/aiops.mdx b/api_docs/aiops.mdx index 6c671e9a34f27..c3a7bcb2ed61f 100644 --- a/api_docs/aiops.mdx +++ b/api_docs/aiops.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/aiops title: "aiops" image: https://source.unsplash.com/400x175/?github description: API docs for the aiops plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'aiops'] --- import aiopsObj from './aiops.devdocs.json'; diff --git a/api_docs/alerting.devdocs.json b/api_docs/alerting.devdocs.json index bb2631b0e32e1..b9e6944723741 100644 --- a/api_docs/alerting.devdocs.json +++ b/api_docs/alerting.devdocs.json @@ -1528,6 +1528,20 @@ "path": "x-pack/plugins/alerting/server/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "alerting", + "id": "def-server.GetSummarizedAlertsFnOpts.excludedAlertInstanceIds", + "type": "Array", + "tags": [], + "label": "excludedAlertInstanceIds", + "description": [], + "signature": [ + "string[]" + ], + "path": "x-pack/plugins/alerting/server/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index 25c277a4b55de..366e9a4f1a601 100644 --- a/api_docs/alerting.mdx +++ b/api_docs/alerting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/alerting title: "alerting" image: https://source.unsplash.com/400x175/?github description: API docs for the alerting plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'alerting'] --- import alertingObj from './alerting.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Response Ops](https://github.com/orgs/elastic/teams/response-ops) for q | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 420 | 0 | 411 | 35 | +| 421 | 0 | 412 | 35 | ## Client diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index 40e55fd4ce915..c4b3fa48bfd75 100644 --- a/api_docs/apm.mdx +++ b/api_docs/apm.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/apm title: "apm" image: https://source.unsplash.com/400x175/?github description: API docs for the apm plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'apm'] --- import apmObj from './apm.devdocs.json'; diff --git a/api_docs/banners.mdx b/api_docs/banners.mdx index 53be76bed12a8..f918cbfe72560 100644 --- a/api_docs/banners.mdx +++ b/api_docs/banners.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/banners title: "banners" image: https://source.unsplash.com/400x175/?github description: API docs for the banners plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'banners'] --- import bannersObj from './banners.devdocs.json'; diff --git a/api_docs/bfetch.mdx b/api_docs/bfetch.mdx index fd0c7d2e91449..319765cb24a95 100644 --- a/api_docs/bfetch.mdx +++ b/api_docs/bfetch.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/bfetch title: "bfetch" image: https://source.unsplash.com/400x175/?github description: API docs for the bfetch plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'bfetch'] --- import bfetchObj from './bfetch.devdocs.json'; diff --git a/api_docs/canvas.mdx b/api_docs/canvas.mdx index 269ed0537221c..c2528bb7fced4 100644 --- a/api_docs/canvas.mdx +++ b/api_docs/canvas.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/canvas title: "canvas" image: https://source.unsplash.com/400x175/?github description: API docs for the canvas plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'canvas'] --- import canvasObj from './canvas.devdocs.json'; diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index 376bfdc4b23ad..4df83673949c8 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cases title: "cases" image: https://source.unsplash.com/400x175/?github description: API docs for the cases plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cases'] --- import casesObj from './cases.devdocs.json'; diff --git a/api_docs/charts.mdx b/api_docs/charts.mdx index 2beb9b1ebebeb..5c6bf12c59637 100644 --- a/api_docs/charts.mdx +++ b/api_docs/charts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/charts title: "charts" image: https://source.unsplash.com/400x175/?github description: API docs for the charts plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'charts'] --- import chartsObj from './charts.devdocs.json'; diff --git a/api_docs/cloud.mdx b/api_docs/cloud.mdx index 1b3c5a5e12203..d5cfc29438715 100644 --- a/api_docs/cloud.mdx +++ b/api_docs/cloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloud title: "cloud" image: https://source.unsplash.com/400x175/?github description: API docs for the cloud plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloud'] --- import cloudObj from './cloud.devdocs.json'; diff --git a/api_docs/cloud_chat.mdx b/api_docs/cloud_chat.mdx index 09db63dfe877b..0f43f2882358f 100644 --- a/api_docs/cloud_chat.mdx +++ b/api_docs/cloud_chat.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudChat title: "cloudChat" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudChat plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudChat'] --- import cloudChatObj from './cloud_chat.devdocs.json'; diff --git a/api_docs/cloud_data_migration.mdx b/api_docs/cloud_data_migration.mdx index dd5cfb5f96955..400902bf0b794 100644 --- a/api_docs/cloud_data_migration.mdx +++ b/api_docs/cloud_data_migration.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudDataMigration title: "cloudDataMigration" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudDataMigration plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDataMigration'] --- import cloudDataMigrationObj from './cloud_data_migration.devdocs.json'; diff --git a/api_docs/cloud_defend.devdocs.json b/api_docs/cloud_defend.devdocs.json new file mode 100644 index 0000000000000..f155214541b73 --- /dev/null +++ b/api_docs/cloud_defend.devdocs.json @@ -0,0 +1,55 @@ +{ + "id": "cloudDefend", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [], + "setup": { + "parentPluginId": "cloudDefend", + "id": "def-public.CloudDefendPluginSetup", + "type": "Interface", + "tags": [], + "label": "CloudDefendPluginSetup", + "description": [], + "path": "x-pack/plugins/cloud_defend/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "parentPluginId": "cloudDefend", + "id": "def-public.CloudDefendPluginStart", + "type": "Interface", + "tags": [], + "label": "CloudDefendPluginStart", + "description": [], + "path": "x-pack/plugins/cloud_defend/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "lifecycle": "start", + "initialIsOpen": true + } + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/cloud_defend.mdx b/api_docs/cloud_defend.mdx new file mode 100644 index 0000000000000..3e37fa4c85314 --- /dev/null +++ b/api_docs/cloud_defend.mdx @@ -0,0 +1,33 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibCloudDefendPluginApi +slug: /kibana-dev-docs/api/cloudDefend +title: "cloudDefend" +image: https://source.unsplash.com/400x175/?github +description: API docs for the cloudDefend plugin +date: 2022-12-20 +tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDefend'] +--- +import cloudDefendObj from './cloud_defend.devdocs.json'; + +Defend for Containers + +Contact [Cloud Native Integrations](https://github.com/orgs/elastic/teams/sec-cloudnative-integrations) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 2 | 0 | 2 | 0 | + +## Client + +### Setup + + +### Start + + diff --git a/api_docs/cloud_experiments.mdx b/api_docs/cloud_experiments.mdx index 8d907a029b64a..979be6ca6d2b0 100644 --- a/api_docs/cloud_experiments.mdx +++ b/api_docs/cloud_experiments.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudExperiments title: "cloudExperiments" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudExperiments plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudExperiments'] --- import cloudExperimentsObj from './cloud_experiments.devdocs.json'; diff --git a/api_docs/cloud_security_posture.mdx b/api_docs/cloud_security_posture.mdx index d7fec137ee6a8..844e465027b68 100644 --- a/api_docs/cloud_security_posture.mdx +++ b/api_docs/cloud_security_posture.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudSecurityPosture title: "cloudSecurityPosture" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudSecurityPosture plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudSecurityPosture'] --- import cloudSecurityPostureObj from './cloud_security_posture.devdocs.json'; diff --git a/api_docs/console.mdx b/api_docs/console.mdx index 67aad2a99e5b5..35ee6f48a23ae 100644 --- a/api_docs/console.mdx +++ b/api_docs/console.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/console title: "console" image: https://source.unsplash.com/400x175/?github description: API docs for the console plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] --- import consoleObj from './console.devdocs.json'; diff --git a/api_docs/controls.devdocs.json b/api_docs/controls.devdocs.json index 7f408645b5cc3..bcd8519dddd92 100644 --- a/api_docs/controls.devdocs.json +++ b/api_docs/controls.devdocs.json @@ -3340,7 +3340,7 @@ "section": "def-public.ControlGroupRendererProps", "text": "ControlGroupRendererProps" }, - "> & { readonly _result: ({ onLoadComplete, getInitialInput, }: ", + "> & { readonly _result: ({ onLoadComplete, getInitialInput, filters, timeRange, query, }: ", { "pluginId": "controls", "scope": "public", @@ -3911,49 +3911,24 @@ "children": [ { "parentPluginId": "controls", - "id": "def-public.ControlGroupRendererProps.onLoadComplete", - "type": "Function", + "id": "def-public.ControlGroupRendererProps.filters", + "type": "Array", "tags": [], - "label": "onLoadComplete", + "label": "filters", "description": [], "signature": [ - "((controlGroup: ", { - "pluginId": "controls", - "scope": "public", - "docId": "kibControlsPluginApi", - "section": "def-public.ControlGroupContainer", - "text": "ControlGroupContainer" + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" }, - ") => void) | undefined" + "[] | undefined" ], "path": "src/plugins/controls/public/control_group/control_group_renderer.tsx", "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "controls", - "id": "def-public.ControlGroupRendererProps.onLoadComplete.$1", - "type": "Object", - "tags": [], - "label": "controlGroup", - "description": [], - "signature": [ - { - "pluginId": "controls", - "scope": "public", - "docId": "kibControlsPluginApi", - "section": "def-public.ControlGroupContainer", - "text": "ControlGroupContainer" - } - ], - "path": "src/plugins/controls/public/control_group/control_group_renderer.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [] + "trackAdoption": false }, { "parentPluginId": "controls", @@ -4125,6 +4100,94 @@ } ], "returnComment": [] + }, + { + "parentPluginId": "controls", + "id": "def-public.ControlGroupRendererProps.onLoadComplete", + "type": "Function", + "tags": [], + "label": "onLoadComplete", + "description": [], + "signature": [ + "((controlGroup: ", + { + "pluginId": "controls", + "scope": "public", + "docId": "kibControlsPluginApi", + "section": "def-public.ControlGroupContainer", + "text": "ControlGroupContainer" + }, + ") => void) | undefined" + ], + "path": "src/plugins/controls/public/control_group/control_group_renderer.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "controls", + "id": "def-public.ControlGroupRendererProps.onLoadComplete.$1", + "type": "Object", + "tags": [], + "label": "controlGroup", + "description": [], + "signature": [ + { + "pluginId": "controls", + "scope": "public", + "docId": "kibControlsPluginApi", + "section": "def-public.ControlGroupContainer", + "text": "ControlGroupContainer" + } + ], + "path": "src/plugins/controls/public/control_group/control_group_renderer.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "controls", + "id": "def-public.ControlGroupRendererProps.timeRange", + "type": "Object", + "tags": [], + "label": "timeRange", + "description": [], + "signature": [ + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.TimeRange", + "text": "TimeRange" + }, + " | undefined" + ], + "path": "src/plugins/controls/public/control_group/control_group_renderer.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "controls", + "id": "def-public.ControlGroupRendererProps.query", + "type": "Object", + "tags": [], + "label": "query", + "description": [], + "signature": [ + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Query", + "text": "Query" + }, + " | undefined" + ], + "path": "src/plugins/controls/public/control_group/control_group_renderer.tsx", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/controls.mdx b/api_docs/controls.mdx index 53ecaa2ea25c1..1453a95df111d 100644 --- a/api_docs/controls.mdx +++ b/api_docs/controls.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/controls title: "controls" image: https://source.unsplash.com/400x175/?github description: API docs for the controls plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'controls'] --- import controlsObj from './controls.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-prese | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 268 | 0 | 259 | 10 | +| 271 | 0 | 262 | 10 | ## Client diff --git a/api_docs/core.devdocs.json b/api_docs/core.devdocs.json index b651421a73903..38c8ac146e4d5 100644 --- a/api_docs/core.devdocs.json +++ b/api_docs/core.devdocs.json @@ -24493,96 +24493,6 @@ ], "initialIsOpen": false }, - { - "parentPluginId": "core", - "id": "def-server.CheckAuthorizationParams", - "type": "Interface", - "tags": [], - "label": "CheckAuthorizationParams", - "description": [ - "\nThe CheckAuthorizationParams interface contains settings for checking\nauthorization via the ISavedObjectsSecurityExtension." - ], - "signature": [ - { - "pluginId": "@kbn/core-saved-objects-server", - "scope": "server", - "docId": "kibKbnCoreSavedObjectsServerPluginApi", - "section": "def-server.CheckAuthorizationParams", - "text": "CheckAuthorizationParams" - }, - "" - ], - "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "core", - "id": "def-server.CheckAuthorizationParams.types", - "type": "Object", - "tags": [], - "label": "types", - "description": [ - "\nA set of types to check." - ], - "signature": [ - "Set" - ], - "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "core", - "id": "def-server.CheckAuthorizationParams.spaces", - "type": "Object", - "tags": [], - "label": "spaces", - "description": [ - "\nA set of spaces to check." - ], - "signature": [ - "Set" - ], - "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "core", - "id": "def-server.CheckAuthorizationParams.actions", - "type": "Object", - "tags": [], - "label": "actions", - "description": [ - "\nAn set of actions to check." - ], - "signature": [ - "Set" - ], - "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "core", - "id": "def-server.CheckAuthorizationParams.options", - "type": "Object", - "tags": [], - "label": "options", - "description": [ - "\nAuthorization options - whether or not to allow global resources, false if options are undefined" - ], - "signature": [ - "{ allowGlobalResource: boolean; } | undefined" - ], - "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, { "parentPluginId": "core", "id": "def-server.CheckAuthorizationResult", @@ -35365,22 +35275,6 @@ "path": "packages/core/http/core-http-server/src/router/socket.ts", "deprecated": false, "trackAdoption": false - }, - { - "parentPluginId": "core", - "id": "def-server.IKibanaSocket.remoteAddress", - "type": "string", - "tags": [], - "label": "remoteAddress", - "description": [ - "\nThe string representation of the remote IP address. For example,`'74.125.127.100'` or\n`'2001:4860:a005::68'`. Value may be `undefined` if the socket is destroyed (for example, if\nthe client disconnected)." - ], - "signature": [ - "string | undefined" - ], - "path": "packages/core/http/core-http-server/src/router/socket.ts", - "deprecated": false, - "trackAdoption": false } ], "initialIsOpen": false @@ -39032,12 +38926,12 @@ "children": [ { "parentPluginId": "core", - "id": "def-server.ISavedObjectsSecurityExtension.checkAuthorization", + "id": "def-server.ISavedObjectsSecurityExtension.performAuthorization", "type": "Function", "tags": [], - "label": "checkAuthorization", + "label": "performAuthorization", "description": [ - "\nChecks authorization of actions on specified types in specified spaces." + "\nPerforms authorization (check & enforce) of actions on specified types in specified spaces." ], "signature": [ "(params: ", @@ -39045,8 +38939,8 @@ "pluginId": "@kbn/core-saved-objects-server", "scope": "server", "docId": "kibKbnCoreSavedObjectsServerPluginApi", - "section": "def-server.CheckAuthorizationParams", - "text": "CheckAuthorizationParams" + "section": "def-server.PerformAuthorizationParams", + "text": "PerformAuthorizationParams" }, ") => Promise<", { @@ -39064,20 +38958,20 @@ "children": [ { "parentPluginId": "core", - "id": "def-server.ISavedObjectsSecurityExtension.checkAuthorization.$1", + "id": "def-server.ISavedObjectsSecurityExtension.performAuthorization.$1", "type": "Object", "tags": [], "label": "params", "description": [ - "- types, spaces, and actions to check" + "- actions, types & spaces map, audit callback, options (enforce bypassed if enforce map is undefined)" ], "signature": [ { "pluginId": "@kbn/core-saved-objects-server", "scope": "server", "docId": "kibKbnCoreSavedObjectsServerPluginApi", - "section": "def-server.CheckAuthorizationParams", - "text": "CheckAuthorizationParams" + "section": "def-server.PerformAuthorizationParams", + "text": "PerformAuthorizationParams" }, "" ], @@ -45555,6 +45449,146 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "core", + "id": "def-server.PerformAuthorizationParams", + "type": "Interface", + "tags": [], + "label": "PerformAuthorizationParams", + "description": [ + "\nThe PerformAuthorizationParams interface contains settings for checking\n& enforcing authorization via the ISavedObjectsSecurityExtension." + ], + "signature": [ + { + "pluginId": "@kbn/core-saved-objects-server", + "scope": "server", + "docId": "kibKbnCoreSavedObjectsServerPluginApi", + "section": "def-server.PerformAuthorizationParams", + "text": "PerformAuthorizationParams" + }, + "" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "core", + "id": "def-server.PerformAuthorizationParams.actions", + "type": "Object", + "tags": [], + "label": "actions", + "description": [ + "\nA set of actions to check." + ], + "signature": [ + "Set" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "core", + "id": "def-server.PerformAuthorizationParams.types", + "type": "Object", + "tags": [], + "label": "types", + "description": [ + "\nA set of types to check." + ], + "signature": [ + "Set" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "core", + "id": "def-server.PerformAuthorizationParams.spaces", + "type": "Object", + "tags": [], + "label": "spaces", + "description": [ + "\nA set of spaces to check (types to check comes from the typesAndSpaces map)." + ], + "signature": [ + "Set" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "core", + "id": "def-server.PerformAuthorizationParams.enforceMap", + "type": "Object", + "tags": [], + "label": "enforceMap", + "description": [ + "\nA map of types (key) to spaces (value) that will be affected by the action(s).\nIf undefined, enforce with be bypassed." + ], + "signature": [ + "Map> | undefined" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "core", + "id": "def-server.PerformAuthorizationParams.auditCallback", + "type": "Function", + "tags": [], + "label": "auditCallback", + "description": [ + "\nA callback intended to handle adding audit events in\nboth error (unauthorized), or success (authorized)\ncases" + ], + "signature": [ + "((error?: Error | undefined) => void) | undefined" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "core", + "id": "def-server.PerformAuthorizationParams.auditCallback.$1", + "type": "Object", + "tags": [], + "label": "error", + "description": [], + "signature": [ + "Error | undefined" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [] + }, + { + "parentPluginId": "core", + "id": "def-server.PerformAuthorizationParams.options", + "type": "Object", + "tags": [], + "label": "options", + "description": [ + "\nAuthorization options\nallowGlobalResource - whether or not to allow global resources, false if options are undefined" + ], + "signature": [ + "{ allowGlobalResource: boolean; } | undefined" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "core", "id": "def-server.Plugin", diff --git a/api_docs/core.mdx b/api_docs/core.mdx index af3ccd167cca4..ba0dd10b962a7 100644 --- a/api_docs/core.mdx +++ b/api_docs/core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/core title: "core" image: https://source.unsplash.com/400x175/?github description: API docs for the core plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core'] --- import coreObj from './core.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2809 | 17 | 1013 | 0 | +| 2811 | 17 | 1014 | 0 | ## Client diff --git a/api_docs/custom_integrations.mdx b/api_docs/custom_integrations.mdx index 8aaa7b1f1c3d9..412d5419fa05c 100644 --- a/api_docs/custom_integrations.mdx +++ b/api_docs/custom_integrations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/customIntegrations title: "customIntegrations" image: https://source.unsplash.com/400x175/?github description: API docs for the customIntegrations plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'customIntegrations'] --- import customIntegrationsObj from './custom_integrations.devdocs.json'; diff --git a/api_docs/dashboard.mdx b/api_docs/dashboard.mdx index 140793152439c..4a9d8671d9e37 100644 --- a/api_docs/dashboard.mdx +++ b/api_docs/dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboard title: "dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboard plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboard'] --- import dashboardObj from './dashboard.devdocs.json'; diff --git a/api_docs/dashboard_enhanced.mdx b/api_docs/dashboard_enhanced.mdx index 6f5261bc7a6c0..3700ba873b0cd 100644 --- a/api_docs/dashboard_enhanced.mdx +++ b/api_docs/dashboard_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboardEnhanced title: "dashboardEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboardEnhanced plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboardEnhanced'] --- import dashboardEnhancedObj from './dashboard_enhanced.devdocs.json'; diff --git a/api_docs/data.devdocs.json b/api_docs/data.devdocs.json index ced8ea708db74..7d5fb1001a667 100644 --- a/api_docs/data.devdocs.json +++ b/api_docs/data.devdocs.json @@ -13013,7 +13013,7 @@ }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/from_filters.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_combined_filter.test.ts" }, { "plugin": "@kbn/es-query", @@ -13021,15 +13021,15 @@ }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/from_kuery.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_filters.test.ts" }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/handle_combined_filter.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_kuery.test.ts" }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_nested_filter.test.ts" }, { "plugin": "@kbn/es-query", @@ -13055,6 +13055,18 @@ "plugin": "@kbn/es-query", "path": "packages/kbn-es-query/src/filters/build_filters/phrases_filter.test.ts" }, + { + "plugin": "@kbn/es-query", + "path": "packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts" + }, + { + "plugin": "@kbn/es-query", + "path": "packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts" + }, + { + "plugin": "@kbn/es-query", + "path": "packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts" + }, { "plugin": "@kbn/es-query", "path": "packages/kbn-es-query/src/kuery/ast/ast.test.ts" @@ -20715,7 +20727,7 @@ }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/from_filters.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_combined_filter.test.ts" }, { "plugin": "@kbn/es-query", @@ -20723,15 +20735,15 @@ }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/from_kuery.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_filters.test.ts" }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/handle_combined_filter.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_kuery.test.ts" }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_nested_filter.test.ts" }, { "plugin": "@kbn/es-query", @@ -20757,6 +20769,18 @@ "plugin": "@kbn/es-query", "path": "packages/kbn-es-query/src/filters/build_filters/phrases_filter.test.ts" }, + { + "plugin": "@kbn/es-query", + "path": "packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts" + }, + { + "plugin": "@kbn/es-query", + "path": "packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts" + }, + { + "plugin": "@kbn/es-query", + "path": "packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts" + }, { "plugin": "@kbn/es-query", "path": "packages/kbn-es-query/src/kuery/ast/ast.test.ts" diff --git a/api_docs/data.mdx b/api_docs/data.mdx index 9993762661870..d0e29344179af 100644 --- a/api_docs/data.mdx +++ b/api_docs/data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data title: "data" image: https://source.unsplash.com/400x175/?github description: API docs for the data plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data'] --- import dataObj from './data.devdocs.json'; diff --git a/api_docs/data_query.devdocs.json b/api_docs/data_query.devdocs.json index 744933b9b734f..c7e0eeb12f5a1 100644 --- a/api_docs/data_query.devdocs.json +++ b/api_docs/data_query.devdocs.json @@ -1756,11 +1756,11 @@ }, ", indexPatterns: ", { - "pluginId": "dataViews", + "pluginId": "@kbn/es-query", "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.DataView", - "text": "DataView" + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewBase", + "text": "DataViewBase" }, "[]) => string" ], @@ -1798,11 +1798,11 @@ "description": [], "signature": [ { - "pluginId": "dataViews", + "pluginId": "@kbn/es-query", "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.DataView", - "text": "DataView" + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewBase", + "text": "DataViewBase" }, "[]" ], @@ -1832,6 +1832,14 @@ "text": "Filter" }, ", indexPatterns: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewBase", + "text": "DataViewBase" + }, + "[] | ", { "pluginId": "dataViews", "scope": "common", @@ -1869,11 +1877,19 @@ { "parentPluginId": "data", "id": "def-public.getFieldDisplayValueFromFilter.$2", - "type": "Array", + "type": "CompoundType", "tags": [], "label": "indexPatterns", "description": [], "signature": [ + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewBase", + "text": "DataViewBase" + }, + "[] | ", { "pluginId": "dataViews", "scope": "common", @@ -1908,23 +1924,7 @@ "section": "def-common.Filter", "text": "Filter" }, - ", indexPatterns: ", - { - "pluginId": "dataViews", - "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.DataView", - "text": "DataView" - }, - "[]) => ", - { - "pluginId": "dataViews", - "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.DataView", - "text": "DataView" - }, - " | undefined" + ", indexPatterns: T[]) => T | undefined" ], "path": "src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts", "deprecated": false, @@ -1959,14 +1959,7 @@ "label": "indexPatterns", "description": [], "signature": [ - { - "pluginId": "dataViews", - "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.DataView", - "text": "DataView" - }, - "[]" + "T[]" ], "path": "src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts", "deprecated": false, diff --git a/api_docs/data_query.mdx b/api_docs/data_query.mdx index 9b86339b353da..356ec3a8d819d 100644 --- a/api_docs/data_query.mdx +++ b/api_docs/data_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-query title: "data.query" image: https://source.unsplash.com/400x175/?github description: API docs for the data.query plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.query'] --- import dataQueryObj from './data_query.devdocs.json'; diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index 51aa208e630f6..90e01cfdc28a4 100644 --- a/api_docs/data_search.mdx +++ b/api_docs/data_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-search title: "data.search" image: https://source.unsplash.com/400x175/?github description: API docs for the data.search plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.search'] --- import dataSearchObj from './data_search.devdocs.json'; diff --git a/api_docs/data_view_editor.mdx b/api_docs/data_view_editor.mdx index ffecfb65a6543..a26e6f46fb5c7 100644 --- a/api_docs/data_view_editor.mdx +++ b/api_docs/data_view_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewEditor title: "dataViewEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewEditor plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewEditor'] --- import dataViewEditorObj from './data_view_editor.devdocs.json'; diff --git a/api_docs/data_view_field_editor.mdx b/api_docs/data_view_field_editor.mdx index e100fd7b1b63d..9b2a14c91eed5 100644 --- a/api_docs/data_view_field_editor.mdx +++ b/api_docs/data_view_field_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewFieldEditor title: "dataViewFieldEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewFieldEditor plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewFieldEditor'] --- import dataViewFieldEditorObj from './data_view_field_editor.devdocs.json'; diff --git a/api_docs/data_view_management.mdx b/api_docs/data_view_management.mdx index 2ef632b6a3254..7856de2224602 100644 --- a/api_docs/data_view_management.mdx +++ b/api_docs/data_view_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewManagement title: "dataViewManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewManagement plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewManagement'] --- import dataViewManagementObj from './data_view_management.devdocs.json'; diff --git a/api_docs/data_views.devdocs.json b/api_docs/data_views.devdocs.json index 3df29041f2824..10a1bffb92de2 100644 --- a/api_docs/data_views.devdocs.json +++ b/api_docs/data_views.devdocs.json @@ -165,7 +165,7 @@ }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/from_filters.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_combined_filter.test.ts" }, { "plugin": "@kbn/es-query", @@ -173,15 +173,15 @@ }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/from_kuery.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_filters.test.ts" }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/handle_combined_filter.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_kuery.test.ts" }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_nested_filter.test.ts" }, { "plugin": "@kbn/es-query", @@ -207,6 +207,18 @@ "plugin": "@kbn/es-query", "path": "packages/kbn-es-query/src/filters/build_filters/phrases_filter.test.ts" }, + { + "plugin": "@kbn/es-query", + "path": "packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts" + }, + { + "plugin": "@kbn/es-query", + "path": "packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts" + }, + { + "plugin": "@kbn/es-query", + "path": "packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts" + }, { "plugin": "@kbn/es-query", "path": "packages/kbn-es-query/src/kuery/ast/ast.test.ts" @@ -8466,7 +8478,7 @@ }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/from_filters.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_combined_filter.test.ts" }, { "plugin": "@kbn/es-query", @@ -8474,15 +8486,15 @@ }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/from_kuery.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_filters.test.ts" }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/handle_combined_filter.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_kuery.test.ts" }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_nested_filter.test.ts" }, { "plugin": "@kbn/es-query", @@ -8508,6 +8520,18 @@ "plugin": "@kbn/es-query", "path": "packages/kbn-es-query/src/filters/build_filters/phrases_filter.test.ts" }, + { + "plugin": "@kbn/es-query", + "path": "packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts" + }, + { + "plugin": "@kbn/es-query", + "path": "packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts" + }, + { + "plugin": "@kbn/es-query", + "path": "packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts" + }, { "plugin": "@kbn/es-query", "path": "packages/kbn-es-query/src/kuery/ast/ast.test.ts" @@ -15848,7 +15872,7 @@ }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/from_filters.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_combined_filter.test.ts" }, { "plugin": "@kbn/es-query", @@ -15856,15 +15880,15 @@ }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/from_kuery.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_filters.test.ts" }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/handle_combined_filter.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_kuery.test.ts" }, { "plugin": "@kbn/es-query", - "path": "packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts" + "path": "packages/kbn-es-query/src/es_query/from_nested_filter.test.ts" }, { "plugin": "@kbn/es-query", @@ -15890,6 +15914,18 @@ "plugin": "@kbn/es-query", "path": "packages/kbn-es-query/src/filters/build_filters/phrases_filter.test.ts" }, + { + "plugin": "@kbn/es-query", + "path": "packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts" + }, + { + "plugin": "@kbn/es-query", + "path": "packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts" + }, + { + "plugin": "@kbn/es-query", + "path": "packages/kbn-es-query/src/filters/helpers/compare_filters.test.ts" + }, { "plugin": "@kbn/es-query", "path": "packages/kbn-es-query/src/kuery/ast/ast.test.ts" diff --git a/api_docs/data_views.mdx b/api_docs/data_views.mdx index a20d552f2f46b..7122b56eaac2b 100644 --- a/api_docs/data_views.mdx +++ b/api_docs/data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViews title: "dataViews" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViews plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViews'] --- import dataViewsObj from './data_views.devdocs.json'; diff --git a/api_docs/data_visualizer.mdx b/api_docs/data_visualizer.mdx index b1f8b5e89bde0..7bf154b4ba3a8 100644 --- a/api_docs/data_visualizer.mdx +++ b/api_docs/data_visualizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataVisualizer title: "dataVisualizer" image: https://source.unsplash.com/400x175/?github description: API docs for the dataVisualizer plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataVisualizer'] --- import dataVisualizerObj from './data_visualizer.devdocs.json'; diff --git a/api_docs/deprecations_by_api.mdx b/api_docs/deprecations_by_api.mdx index 76e4d640decf0..f26e5e9594a17 100644 --- a/api_docs/deprecations_by_api.mdx +++ b/api_docs/deprecations_by_api.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByApi slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-api title: Deprecated API usage by API description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index fe214a1aef2b2..70158ccf3f871 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByPlugin slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-plugin title: Deprecated API usage by plugin description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -165,9 +165,9 @@ so TS and code-reference navigation might not highlight them. | | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| -| | [types.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/types.ts#:~:text=title), [build_es_query.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/build_es_query.test.ts#:~:text=title), [from_filters.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_filters.test.ts#:~:text=title), [from_filters.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_filters.test.ts#:~:text=title), [from_kuery.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_kuery.test.ts#:~:text=title), [handle_combined_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/handle_combined_filter.test.ts#:~:text=title), [handle_nested_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts#:~:text=title), [build_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/build_filter.test.ts#:~:text=title), [exists_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/exists_filter.test.ts#:~:text=title), [get_filter_field.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/get_filter_field.test.ts#:~:text=title)+ 36 more | - | -| | [types.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/types.ts#:~:text=title), [build_es_query.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/build_es_query.test.ts#:~:text=title), [from_filters.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_filters.test.ts#:~:text=title), [from_filters.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_filters.test.ts#:~:text=title), [from_kuery.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_kuery.test.ts#:~:text=title), [handle_combined_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/handle_combined_filter.test.ts#:~:text=title), [handle_nested_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts#:~:text=title), [build_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/build_filter.test.ts#:~:text=title), [exists_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/exists_filter.test.ts#:~:text=title), [get_filter_field.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/get_filter_field.test.ts#:~:text=title)+ 36 more | - | -| | [types.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/types.ts#:~:text=title), [build_es_query.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/build_es_query.test.ts#:~:text=title), [from_filters.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_filters.test.ts#:~:text=title), [from_filters.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_filters.test.ts#:~:text=title), [from_kuery.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_kuery.test.ts#:~:text=title), [handle_combined_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/handle_combined_filter.test.ts#:~:text=title), [handle_nested_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts#:~:text=title), [build_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/build_filter.test.ts#:~:text=title), [exists_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/exists_filter.test.ts#:~:text=title), [get_filter_field.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/get_filter_field.test.ts#:~:text=title)+ 13 more | - | +| | [types.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/types.ts#:~:text=title), [build_es_query.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/build_es_query.test.ts#:~:text=title), [from_combined_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_combined_filter.test.ts#:~:text=title), [from_filters.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_filters.test.ts#:~:text=title), [from_filters.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_filters.test.ts#:~:text=title), [from_kuery.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_kuery.test.ts#:~:text=title), [from_nested_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_nested_filter.test.ts#:~:text=title), [build_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/build_filter.test.ts#:~:text=title), [exists_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/exists_filter.test.ts#:~:text=title), [get_filter_field.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/get_filter_field.test.ts#:~:text=title)+ 42 more | - | +| | [types.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/types.ts#:~:text=title), [build_es_query.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/build_es_query.test.ts#:~:text=title), [from_combined_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_combined_filter.test.ts#:~:text=title), [from_filters.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_filters.test.ts#:~:text=title), [from_filters.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_filters.test.ts#:~:text=title), [from_kuery.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_kuery.test.ts#:~:text=title), [from_nested_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_nested_filter.test.ts#:~:text=title), [build_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/build_filter.test.ts#:~:text=title), [exists_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/exists_filter.test.ts#:~:text=title), [get_filter_field.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/get_filter_field.test.ts#:~:text=title)+ 42 more | - | +| | [types.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/types.ts#:~:text=title), [build_es_query.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/build_es_query.test.ts#:~:text=title), [from_combined_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_combined_filter.test.ts#:~:text=title), [from_filters.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_filters.test.ts#:~:text=title), [from_filters.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_filters.test.ts#:~:text=title), [from_kuery.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_kuery.test.ts#:~:text=title), [from_nested_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/es_query/from_nested_filter.test.ts#:~:text=title), [build_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/build_filter.test.ts#:~:text=title), [exists_filter.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/exists_filter.test.ts#:~:text=title), [get_filter_field.test.ts](https://github.com/elastic/kibana/tree/main/packages/kbn-es-query/src/filters/build_filters/get_filter_field.test.ts#:~:text=title)+ 16 more | - | diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index af3f25c75d1e9..c9244ae77e9cb 100644 --- a/api_docs/deprecations_by_team.mdx +++ b/api_docs/deprecations_by_team.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsDueByTeam slug: /kibana-dev-docs/api-meta/deprecations-due-by-team title: Deprecated APIs due to be removed, by team description: Lists the teams that are referencing deprecated APIs with a remove by date. -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index 8a3694acb49af..f8584c191894b 100644 --- a/api_docs/dev_tools.mdx +++ b/api_docs/dev_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/devTools title: "devTools" image: https://source.unsplash.com/400x175/?github description: API docs for the devTools plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'devTools'] --- import devToolsObj from './dev_tools.devdocs.json'; diff --git a/api_docs/discover.mdx b/api_docs/discover.mdx index 238bd9e21fb84..cc69aa28bd5f9 100644 --- a/api_docs/discover.mdx +++ b/api_docs/discover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discover title: "discover" image: https://source.unsplash.com/400x175/?github description: API docs for the discover plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discover'] --- import discoverObj from './discover.devdocs.json'; diff --git a/api_docs/discover_enhanced.mdx b/api_docs/discover_enhanced.mdx index 69b5c2db6c08e..0031afbf5a82e 100644 --- a/api_docs/discover_enhanced.mdx +++ b/api_docs/discover_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discoverEnhanced title: "discoverEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the discoverEnhanced plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverEnhanced'] --- import discoverEnhancedObj from './discover_enhanced.devdocs.json'; diff --git a/api_docs/embeddable.devdocs.json b/api_docs/embeddable.devdocs.json index cb912b4de95a8..8732fa7880141 100644 --- a/api_docs/embeddable.devdocs.json +++ b/api_docs/embeddable.devdocs.json @@ -7179,15 +7179,31 @@ "tags": [], "label": "getExplicitInput", "description": [ - "\nCan be used to request explicit input from the user, to be passed in to `EmbeddableFactory:create`.\nExplicit input is stored on the parent container for this embeddable. It overrides all inherited\ninput passed down from the parent container." + "\nCan be used to request explicit input from the user, to be passed in to `EmbeddableFactory:create`.\nExplicit input is stored on the parent container for this embeddable. It overrides all inherited\ninput passed down from the parent container.\n\nCan be used to edit an embeddable by re-requesting explicit input. Initial input can be provided to allow the editor to show the current state." ], "signature": [ - "() => Promise>" + "(initialInput?: Partial | undefined) => Promise>" ], "path": "src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts", "deprecated": false, "trackAdoption": false, - "children": [], + "children": [ + { + "parentPluginId": "embeddable", + "id": "def-public.EmbeddableFactory.getExplicitInput.$1", + "type": "Object", + "tags": [], + "label": "initialInput", + "description": [], + "signature": [ + "Partial | undefined" + ], + "path": "src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], "returnComment": [] }, { @@ -7601,6 +7617,20 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "embeddable", + "id": "def-public.EmbeddableOutput.editableWithExplicitInput", + "type": "CompoundType", + "tags": [], + "label": "editableWithExplicitInput", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "embeddable", "id": "def-public.EmbeddableOutput.savedObjectId", diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index 9036b1713f6e0..84ddc9c0e6053 100644 --- a/api_docs/embeddable.mdx +++ b/api_docs/embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddable title: "embeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddable plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddable'] --- import embeddableObj from './embeddable.devdocs.json'; @@ -21,7 +21,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 522 | 8 | 421 | 4 | +| 524 | 8 | 423 | 4 | ## Client diff --git a/api_docs/embeddable_enhanced.mdx b/api_docs/embeddable_enhanced.mdx index cbf80d4882130..19a18da603606 100644 --- a/api_docs/embeddable_enhanced.mdx +++ b/api_docs/embeddable_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddableEnhanced title: "embeddableEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddableEnhanced plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddableEnhanced'] --- import embeddableEnhancedObj from './embeddable_enhanced.devdocs.json'; diff --git a/api_docs/encrypted_saved_objects.mdx b/api_docs/encrypted_saved_objects.mdx index 5f0808b340566..9f07975955d5e 100644 --- a/api_docs/encrypted_saved_objects.mdx +++ b/api_docs/encrypted_saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/encryptedSavedObjects title: "encryptedSavedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the encryptedSavedObjects plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'encryptedSavedObjects'] --- import encryptedSavedObjectsObj from './encrypted_saved_objects.devdocs.json'; diff --git a/api_docs/enterprise_search.mdx b/api_docs/enterprise_search.mdx index a0fcf90959eeb..eb36cf7680865 100644 --- a/api_docs/enterprise_search.mdx +++ b/api_docs/enterprise_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/enterpriseSearch title: "enterpriseSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the enterpriseSearch plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'enterpriseSearch'] --- import enterpriseSearchObj from './enterprise_search.devdocs.json'; diff --git a/api_docs/es_ui_shared.mdx b/api_docs/es_ui_shared.mdx index 305179b2aa871..2e4cf2d78b432 100644 --- a/api_docs/es_ui_shared.mdx +++ b/api_docs/es_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/esUiShared title: "esUiShared" image: https://source.unsplash.com/400x175/?github description: API docs for the esUiShared plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'esUiShared'] --- import esUiSharedObj from './es_ui_shared.devdocs.json'; diff --git a/api_docs/event_annotation.mdx b/api_docs/event_annotation.mdx index 0436f1c723079..1c7641351228a 100644 --- a/api_docs/event_annotation.mdx +++ b/api_docs/event_annotation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventAnnotation title: "eventAnnotation" image: https://source.unsplash.com/400x175/?github description: API docs for the eventAnnotation plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventAnnotation'] --- import eventAnnotationObj from './event_annotation.devdocs.json'; diff --git a/api_docs/event_log.mdx b/api_docs/event_log.mdx index fa42d391bf715..22de94d18660d 100644 --- a/api_docs/event_log.mdx +++ b/api_docs/event_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventLog title: "eventLog" image: https://source.unsplash.com/400x175/?github description: API docs for the eventLog plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventLog'] --- import eventLogObj from './event_log.devdocs.json'; diff --git a/api_docs/expression_error.mdx b/api_docs/expression_error.mdx index ab70290265cad..d2a982e3c3bd2 100644 --- a/api_docs/expression_error.mdx +++ b/api_docs/expression_error.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionError title: "expressionError" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionError plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionError'] --- import expressionErrorObj from './expression_error.devdocs.json'; diff --git a/api_docs/expression_gauge.mdx b/api_docs/expression_gauge.mdx index 2c589527edd48..ea13b3b9442d4 100644 --- a/api_docs/expression_gauge.mdx +++ b/api_docs/expression_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionGauge title: "expressionGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionGauge plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionGauge'] --- import expressionGaugeObj from './expression_gauge.devdocs.json'; diff --git a/api_docs/expression_heatmap.mdx b/api_docs/expression_heatmap.mdx index 1799e407926a4..ecb1cf3c4d631 100644 --- a/api_docs/expression_heatmap.mdx +++ b/api_docs/expression_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionHeatmap title: "expressionHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionHeatmap plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionHeatmap'] --- import expressionHeatmapObj from './expression_heatmap.devdocs.json'; diff --git a/api_docs/expression_image.mdx b/api_docs/expression_image.mdx index 8da3e9661bdd1..49accc341591e 100644 --- a/api_docs/expression_image.mdx +++ b/api_docs/expression_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionImage title: "expressionImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionImage plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionImage'] --- import expressionImageObj from './expression_image.devdocs.json'; diff --git a/api_docs/expression_legacy_metric_vis.mdx b/api_docs/expression_legacy_metric_vis.mdx index b344d9c694217..4647b6010ca2c 100644 --- a/api_docs/expression_legacy_metric_vis.mdx +++ b/api_docs/expression_legacy_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionLegacyMetricVis title: "expressionLegacyMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionLegacyMetricVis plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionLegacyMetricVis'] --- import expressionLegacyMetricVisObj from './expression_legacy_metric_vis.devdocs.json'; diff --git a/api_docs/expression_metric.mdx b/api_docs/expression_metric.mdx index 5ae7ae44489ee..01e0ead3900a5 100644 --- a/api_docs/expression_metric.mdx +++ b/api_docs/expression_metric.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetric title: "expressionMetric" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetric plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetric'] --- import expressionMetricObj from './expression_metric.devdocs.json'; diff --git a/api_docs/expression_metric_vis.mdx b/api_docs/expression_metric_vis.mdx index f8597b25487fb..6bc56e28373c3 100644 --- a/api_docs/expression_metric_vis.mdx +++ b/api_docs/expression_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetricVis title: "expressionMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetricVis plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetricVis'] --- import expressionMetricVisObj from './expression_metric_vis.devdocs.json'; diff --git a/api_docs/expression_partition_vis.mdx b/api_docs/expression_partition_vis.mdx index 7630444c201c6..78d637da70f81 100644 --- a/api_docs/expression_partition_vis.mdx +++ b/api_docs/expression_partition_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionPartitionVis title: "expressionPartitionVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionPartitionVis plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionPartitionVis'] --- import expressionPartitionVisObj from './expression_partition_vis.devdocs.json'; diff --git a/api_docs/expression_repeat_image.mdx b/api_docs/expression_repeat_image.mdx index 9e68c12721dc2..f1701e24c59d4 100644 --- a/api_docs/expression_repeat_image.mdx +++ b/api_docs/expression_repeat_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRepeatImage title: "expressionRepeatImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRepeatImage plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRepeatImage'] --- import expressionRepeatImageObj from './expression_repeat_image.devdocs.json'; diff --git a/api_docs/expression_reveal_image.mdx b/api_docs/expression_reveal_image.mdx index adf7f87deff1b..7b348bb6e4744 100644 --- a/api_docs/expression_reveal_image.mdx +++ b/api_docs/expression_reveal_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRevealImage title: "expressionRevealImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRevealImage plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRevealImage'] --- import expressionRevealImageObj from './expression_reveal_image.devdocs.json'; diff --git a/api_docs/expression_shape.mdx b/api_docs/expression_shape.mdx index 48ebfdbf411dc..4a66609653292 100644 --- a/api_docs/expression_shape.mdx +++ b/api_docs/expression_shape.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionShape title: "expressionShape" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionShape plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionShape'] --- import expressionShapeObj from './expression_shape.devdocs.json'; diff --git a/api_docs/expression_tagcloud.mdx b/api_docs/expression_tagcloud.mdx index 69e8b934c868d..4d2fa25c262e8 100644 --- a/api_docs/expression_tagcloud.mdx +++ b/api_docs/expression_tagcloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionTagcloud title: "expressionTagcloud" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionTagcloud plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionTagcloud'] --- import expressionTagcloudObj from './expression_tagcloud.devdocs.json'; diff --git a/api_docs/expression_x_y.mdx b/api_docs/expression_x_y.mdx index 8c9214ff3fcdf..a1ecd7abcb70a 100644 --- a/api_docs/expression_x_y.mdx +++ b/api_docs/expression_x_y.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionXY title: "expressionXY" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionXY plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionXY'] --- import expressionXYObj from './expression_x_y.devdocs.json'; diff --git a/api_docs/expressions.mdx b/api_docs/expressions.mdx index 7fe9c4c3d2610..7cfa3a29be8f5 100644 --- a/api_docs/expressions.mdx +++ b/api_docs/expressions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressions title: "expressions" image: https://source.unsplash.com/400x175/?github description: API docs for the expressions plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressions'] --- import expressionsObj from './expressions.devdocs.json'; diff --git a/api_docs/features.mdx b/api_docs/features.mdx index 74dbfa020e1cb..bb474c46c4884 100644 --- a/api_docs/features.mdx +++ b/api_docs/features.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/features title: "features" image: https://source.unsplash.com/400x175/?github description: API docs for the features plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'features'] --- import featuresObj from './features.devdocs.json'; diff --git a/api_docs/field_formats.mdx b/api_docs/field_formats.mdx index 72d4408e11fc4..f137e5a686b9f 100644 --- a/api_docs/field_formats.mdx +++ b/api_docs/field_formats.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fieldFormats title: "fieldFormats" image: https://source.unsplash.com/400x175/?github description: API docs for the fieldFormats plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fieldFormats'] --- import fieldFormatsObj from './field_formats.devdocs.json'; diff --git a/api_docs/file_upload.mdx b/api_docs/file_upload.mdx index 51264e812068f..705285aa713f8 100644 --- a/api_docs/file_upload.mdx +++ b/api_docs/file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fileUpload title: "fileUpload" image: https://source.unsplash.com/400x175/?github description: API docs for the fileUpload plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fileUpload'] --- import fileUploadObj from './file_upload.devdocs.json'; diff --git a/api_docs/files.devdocs.json b/api_docs/files.devdocs.json index 664a95623f5a9..193350e129e16 100644 --- a/api_docs/files.devdocs.json +++ b/api_docs/files.devdocs.json @@ -600,6 +600,10 @@ { "plugin": "filesManagement", "path": "src/plugins/files_management/public/mount_management_section.tsx" + }, + { + "plugin": "imageEmbeddable", + "path": "src/plugins/image_embeddable/public/plugin.ts" } ] }, diff --git a/api_docs/files.mdx b/api_docs/files.mdx index 1ae4e984f6ebc..cda69f011aef6 100644 --- a/api_docs/files.mdx +++ b/api_docs/files.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/files title: "files" image: https://source.unsplash.com/400x175/?github description: API docs for the files plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'files'] --- import filesObj from './files.devdocs.json'; diff --git a/api_docs/files_management.mdx b/api_docs/files_management.mdx index 580137f8e29ba..acb4107ed5ef7 100644 --- a/api_docs/files_management.mdx +++ b/api_docs/files_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/filesManagement title: "filesManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the filesManagement plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'filesManagement'] --- import filesManagementObj from './files_management.devdocs.json'; diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index f8db6924db67a..5ed532d735777 100644 --- a/api_docs/fleet.mdx +++ b/api_docs/fleet.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fleet title: "fleet" image: https://source.unsplash.com/400x175/?github description: API docs for the fleet plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fleet'] --- import fleetObj from './fleet.devdocs.json'; diff --git a/api_docs/global_search.mdx b/api_docs/global_search.mdx index 776d9689ebbf5..7e1dbcd2e6339 100644 --- a/api_docs/global_search.mdx +++ b/api_docs/global_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/globalSearch title: "globalSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the globalSearch plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'globalSearch'] --- import globalSearchObj from './global_search.devdocs.json'; diff --git a/api_docs/guided_onboarding.mdx b/api_docs/guided_onboarding.mdx index bff609ac08a29..d9b664842b6c8 100644 --- a/api_docs/guided_onboarding.mdx +++ b/api_docs/guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/guidedOnboarding title: "guidedOnboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the guidedOnboarding plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'guidedOnboarding'] --- import guidedOnboardingObj from './guided_onboarding.devdocs.json'; diff --git a/api_docs/home.mdx b/api_docs/home.mdx index 4a99fb03335b7..96b0e312ad346 100644 --- a/api_docs/home.mdx +++ b/api_docs/home.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/home title: "home" image: https://source.unsplash.com/400x175/?github description: API docs for the home plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'home'] --- import homeObj from './home.devdocs.json'; diff --git a/api_docs/index_lifecycle_management.mdx b/api_docs/index_lifecycle_management.mdx index 63c23d9f326f1..748af83efb536 100644 --- a/api_docs/index_lifecycle_management.mdx +++ b/api_docs/index_lifecycle_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexLifecycleManagement title: "indexLifecycleManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexLifecycleManagement plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexLifecycleManagement'] --- import indexLifecycleManagementObj from './index_lifecycle_management.devdocs.json'; diff --git a/api_docs/index_management.mdx b/api_docs/index_management.mdx index 7cfa122744dbd..136a9c2d8abc0 100644 --- a/api_docs/index_management.mdx +++ b/api_docs/index_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexManagement title: "indexManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexManagement plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexManagement'] --- import indexManagementObj from './index_management.devdocs.json'; diff --git a/api_docs/infra.mdx b/api_docs/infra.mdx index 44228b7b5e5fc..3f31c99efb4c1 100644 --- a/api_docs/infra.mdx +++ b/api_docs/infra.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/infra title: "infra" image: https://source.unsplash.com/400x175/?github description: API docs for the infra plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'infra'] --- import infraObj from './infra.devdocs.json'; diff --git a/api_docs/inspector.mdx b/api_docs/inspector.mdx index b6c6d0b212703..8dedf4eccffa0 100644 --- a/api_docs/inspector.mdx +++ b/api_docs/inspector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/inspector title: "inspector" image: https://source.unsplash.com/400x175/?github description: API docs for the inspector plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'inspector'] --- import inspectorObj from './inspector.devdocs.json'; diff --git a/api_docs/interactive_setup.mdx b/api_docs/interactive_setup.mdx index e3b723cffbff3..8a1a1a45c5567 100644 --- a/api_docs/interactive_setup.mdx +++ b/api_docs/interactive_setup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/interactiveSetup title: "interactiveSetup" image: https://source.unsplash.com/400x175/?github description: API docs for the interactiveSetup plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'interactiveSetup'] --- import interactiveSetupObj from './interactive_setup.devdocs.json'; diff --git a/api_docs/kbn_ace.mdx b/api_docs/kbn_ace.mdx index 00a1abc16a8bb..7dd9406e62857 100644 --- a/api_docs/kbn_ace.mdx +++ b/api_docs/kbn_ace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ace title: "@kbn/ace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ace plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ace'] --- import kbnAceObj from './kbn_ace.devdocs.json'; diff --git a/api_docs/kbn_aiops_components.mdx b/api_docs/kbn_aiops_components.mdx index 5be7193df271c..49fb3b4cbfea1 100644 --- a/api_docs/kbn_aiops_components.mdx +++ b/api_docs/kbn_aiops_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-components title: "@kbn/aiops-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-components plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-components'] --- import kbnAiopsComponentsObj from './kbn_aiops_components.devdocs.json'; diff --git a/api_docs/kbn_aiops_utils.mdx b/api_docs/kbn_aiops_utils.mdx index ad2bc808fb55c..8047b934bad21 100644 --- a/api_docs/kbn_aiops_utils.mdx +++ b/api_docs/kbn_aiops_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-utils title: "@kbn/aiops-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-utils plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-utils'] --- import kbnAiopsUtilsObj from './kbn_aiops_utils.devdocs.json'; diff --git a/api_docs/kbn_alerts.mdx b/api_docs/kbn_alerts.mdx index 762c68662f6fc..4b529e782b57d 100644 --- a/api_docs/kbn_alerts.mdx +++ b/api_docs/kbn_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts title: "@kbn/alerts" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts'] --- import kbnAlertsObj from './kbn_alerts.devdocs.json'; diff --git a/api_docs/kbn_analytics.mdx b/api_docs/kbn_analytics.mdx index 9b51869f2f860..e78c8a7fab299 100644 --- a/api_docs/kbn_analytics.mdx +++ b/api_docs/kbn_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics title: "@kbn/analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics'] --- import kbnAnalyticsObj from './kbn_analytics.devdocs.json'; diff --git a/api_docs/kbn_analytics_client.mdx b/api_docs/kbn_analytics_client.mdx index 0fc22fc6a3dee..880378b9fa61f 100644 --- a/api_docs/kbn_analytics_client.mdx +++ b/api_docs/kbn_analytics_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-client title: "@kbn/analytics-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-client plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-client'] --- import kbnAnalyticsClientObj from './kbn_analytics_client.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx index d5f5ff9cd0400..bcec456c3073e 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-browser title: "@kbn/analytics-shippers-elastic-v3-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-browser'] --- import kbnAnalyticsShippersElasticV3BrowserObj from './kbn_analytics_shippers_elastic_v3_browser.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx index fd8c25dae28fb..846ebb476e941 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-common title: "@kbn/analytics-shippers-elastic-v3-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-common plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-common'] --- import kbnAnalyticsShippersElasticV3CommonObj from './kbn_analytics_shippers_elastic_v3_common.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx index ef0f8a14f2715..3e68a82a4933a 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-server title: "@kbn/analytics-shippers-elastic-v3-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-server'] --- import kbnAnalyticsShippersElasticV3ServerObj from './kbn_analytics_shippers_elastic_v3_server.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_fullstory.mdx b/api_docs/kbn_analytics_shippers_fullstory.mdx index a3d6354d92ba6..59b7a0b1066e7 100644 --- a/api_docs/kbn_analytics_shippers_fullstory.mdx +++ b/api_docs/kbn_analytics_shippers_fullstory.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-fullstory title: "@kbn/analytics-shippers-fullstory" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-fullstory plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-fullstory'] --- import kbnAnalyticsShippersFullstoryObj from './kbn_analytics_shippers_fullstory.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_gainsight.mdx b/api_docs/kbn_analytics_shippers_gainsight.mdx index fac93033a0b02..eb642747b4e48 100644 --- a/api_docs/kbn_analytics_shippers_gainsight.mdx +++ b/api_docs/kbn_analytics_shippers_gainsight.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-gainsight title: "@kbn/analytics-shippers-gainsight" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-gainsight plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-gainsight'] --- import kbnAnalyticsShippersGainsightObj from './kbn_analytics_shippers_gainsight.devdocs.json'; diff --git a/api_docs/kbn_apm_config_loader.mdx b/api_docs/kbn_apm_config_loader.mdx index e3b37a0be3275..1010ac5e2dd97 100644 --- a/api_docs/kbn_apm_config_loader.mdx +++ b/api_docs/kbn_apm_config_loader.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-config-loader title: "@kbn/apm-config-loader" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-config-loader plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-config-loader'] --- import kbnApmConfigLoaderObj from './kbn_apm_config_loader.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace.mdx b/api_docs/kbn_apm_synthtrace.mdx index 785acc7b96fec..0bc98e042151b 100644 --- a/api_docs/kbn_apm_synthtrace.mdx +++ b/api_docs/kbn_apm_synthtrace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace title: "@kbn/apm-synthtrace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace'] --- import kbnApmSynthtraceObj from './kbn_apm_synthtrace.devdocs.json'; diff --git a/api_docs/kbn_apm_utils.mdx b/api_docs/kbn_apm_utils.mdx index d9b06a61054f7..a3360dd325850 100644 --- a/api_docs/kbn_apm_utils.mdx +++ b/api_docs/kbn_apm_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-utils title: "@kbn/apm-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-utils plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-utils'] --- import kbnApmUtilsObj from './kbn_apm_utils.devdocs.json'; diff --git a/api_docs/kbn_axe_config.mdx b/api_docs/kbn_axe_config.mdx index f52f12fddbed6..79ff4791bfb33 100644 --- a/api_docs/kbn_axe_config.mdx +++ b/api_docs/kbn_axe_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-axe-config title: "@kbn/axe-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/axe-config plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/axe-config'] --- import kbnAxeConfigObj from './kbn_axe_config.devdocs.json'; diff --git a/api_docs/kbn_cases_components.mdx b/api_docs/kbn_cases_components.mdx index 6ce576b5dcf12..07fd64a5eac48 100644 --- a/api_docs/kbn_cases_components.mdx +++ b/api_docs/kbn_cases_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cases-components title: "@kbn/cases-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cases-components plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cases-components'] --- import kbnCasesComponentsObj from './kbn_cases_components.devdocs.json'; diff --git a/api_docs/kbn_chart_icons.mdx b/api_docs/kbn_chart_icons.mdx index b4c4ddef5e1e7..b45945a95457b 100644 --- a/api_docs/kbn_chart_icons.mdx +++ b/api_docs/kbn_chart_icons.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-icons title: "@kbn/chart-icons" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-icons plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-icons'] --- import kbnChartIconsObj from './kbn_chart_icons.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_core.mdx b/api_docs/kbn_ci_stats_core.mdx index 52241910be723..3fceeea493c6d 100644 --- a/api_docs/kbn_ci_stats_core.mdx +++ b/api_docs/kbn_ci_stats_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-core title: "@kbn/ci-stats-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-core plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-core'] --- import kbnCiStatsCoreObj from './kbn_ci_stats_core.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_performance_metrics.mdx b/api_docs/kbn_ci_stats_performance_metrics.mdx index 1123449eb7455..dc2720e699358 100644 --- a/api_docs/kbn_ci_stats_performance_metrics.mdx +++ b/api_docs/kbn_ci_stats_performance_metrics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-performance-metrics title: "@kbn/ci-stats-performance-metrics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-performance-metrics plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-performance-metrics'] --- import kbnCiStatsPerformanceMetricsObj from './kbn_ci_stats_performance_metrics.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_reporter.mdx b/api_docs/kbn_ci_stats_reporter.mdx index 888b05c42cdaf..ffd42db8b26fe 100644 --- a/api_docs/kbn_ci_stats_reporter.mdx +++ b/api_docs/kbn_ci_stats_reporter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-reporter title: "@kbn/ci-stats-reporter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-reporter plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-reporter'] --- import kbnCiStatsReporterObj from './kbn_ci_stats_reporter.devdocs.json'; diff --git a/api_docs/kbn_cli_dev_mode.mdx b/api_docs/kbn_cli_dev_mode.mdx index f6b70aa1cf95b..6aba149726403 100644 --- a/api_docs/kbn_cli_dev_mode.mdx +++ b/api_docs/kbn_cli_dev_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cli-dev-mode title: "@kbn/cli-dev-mode" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cli-dev-mode plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cli-dev-mode'] --- import kbnCliDevModeObj from './kbn_cli_dev_mode.devdocs.json'; diff --git a/api_docs/kbn_coloring.mdx b/api_docs/kbn_coloring.mdx index 39b1d65a747a9..fc5aac790de65 100644 --- a/api_docs/kbn_coloring.mdx +++ b/api_docs/kbn_coloring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-coloring title: "@kbn/coloring" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/coloring plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/coloring'] --- import kbnColoringObj from './kbn_coloring.devdocs.json'; diff --git a/api_docs/kbn_config.mdx b/api_docs/kbn_config.mdx index 49bdaff30bab5..c8d1585b4b6a2 100644 --- a/api_docs/kbn_config.mdx +++ b/api_docs/kbn_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config title: "@kbn/config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config'] --- import kbnConfigObj from './kbn_config.devdocs.json'; diff --git a/api_docs/kbn_config_mocks.mdx b/api_docs/kbn_config_mocks.mdx index 8c3e780c70842..176933bf1c908 100644 --- a/api_docs/kbn_config_mocks.mdx +++ b/api_docs/kbn_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-mocks title: "@kbn/config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-mocks'] --- import kbnConfigMocksObj from './kbn_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_config_schema.mdx b/api_docs/kbn_config_schema.mdx index 1c2a816c234f3..c591987bdd506 100644 --- a/api_docs/kbn_config_schema.mdx +++ b/api_docs/kbn_config_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-schema title: "@kbn/config-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-schema plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-schema'] --- import kbnConfigSchemaObj from './kbn_config_schema.devdocs.json'; diff --git a/api_docs/kbn_content_management_content_editor.mdx b/api_docs/kbn_content_management_content_editor.mdx index bb301b345eb10..d2898bb4fa531 100644 --- a/api_docs/kbn_content_management_content_editor.mdx +++ b/api_docs/kbn_content_management_content_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-content-editor title: "@kbn/content-management-content-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-content-editor plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-content-editor'] --- import kbnContentManagementContentEditorObj from './kbn_content_management_content_editor.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list.devdocs.json b/api_docs/kbn_content_management_table_list.devdocs.json index cd7801e4b598c..937dddb17c4de 100644 --- a/api_docs/kbn_content_management_table_list.devdocs.json +++ b/api_docs/kbn_content_management_table_list.devdocs.json @@ -35,7 +35,7 @@ "section": "def-common.UserContentCommonSchema", "text": "UserContentCommonSchema" }, - ">({ tableListTitle, tableListDescription, entityName, entityNamePlural, initialFilter: initialQuery, headingId, initialPageSize, listingLimit, customTableColumn, emptyPrompt, findItems, createItem, editItem, deleteItems, getDetailViewLink, onClickTitle, id, contentEditor, children, titleColumnName, additionalRightSideActions, withoutPageTemplateWrapper, }: ", + ">({ tableListTitle, tableListDescription, entityName, entityNamePlural, initialFilter: initialQuery, headingId, initialPageSize, listingLimit, urlStateEnabled, customTableColumn, emptyPrompt, findItems, createItem, editItem, deleteItems, getDetailViewLink, onClickTitle, id: listingId, contentEditor, children, titleColumnName, additionalRightSideActions, withoutPageTemplateWrapper, }: ", "Props", ") => JSX.Element | null" ], @@ -305,11 +305,11 @@ "Tag", ") => void) | undefined; tagRender?: ((tag: ", "Tag", - ") => JSX.Element) | undefined; }>; SavedObjectSaveModalTagSelector: React.FC<{ initialSelection: string[]; onTagsSelected: (ids: string[]) => void; }>; }; parseSearchQuery: (query: string, options?: { useName?: boolean | undefined; tagField?: string | undefined; } | undefined) => { searchTerm: string; tagReferences: ", + ") => JSX.Element) | undefined; }>; SavedObjectSaveModalTagSelector: React.FC<{ initialSelection: string[]; onTagsSelected: (ids: string[]) => void; }>; }; parseSearchQuery: (query: string, options?: { useName?: boolean | undefined; tagField?: string | undefined; } | undefined) => Promise<{ searchTerm: string; tagReferences: ", "SavedObjectsFindOptionsReference", "[]; tagReferencesToExclude: ", "SavedObjectsFindOptionsReference", - "[]; valid: boolean; }; getTagList: () => ", + "[]; valid: boolean; }>; getTagList: () => ", "Tag", "[]; getTagIdsFromReferences: (references: ", "SavedObjectsReference", diff --git a/api_docs/kbn_content_management_table_list.mdx b/api_docs/kbn_content_management_table_list.mdx index 7698b1d89ebbc..9046b519896c1 100644 --- a/api_docs/kbn_content_management_table_list.mdx +++ b/api_docs/kbn_content_management_table_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-table-list title: "@kbn/content-management-table-list" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-table-list plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list'] --- import kbnContentManagementTableListObj from './kbn_content_management_table_list.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser.mdx b/api_docs/kbn_core_analytics_browser.mdx index 88a678e379437..08d136274b990 100644 --- a/api_docs/kbn_core_analytics_browser.mdx +++ b/api_docs/kbn_core_analytics_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser title: "@kbn/core-analytics-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser'] --- import kbnCoreAnalyticsBrowserObj from './kbn_core_analytics_browser.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_internal.mdx b/api_docs/kbn_core_analytics_browser_internal.mdx index 201fbd7991207..395edcfa5a630 100644 --- a/api_docs/kbn_core_analytics_browser_internal.mdx +++ b/api_docs/kbn_core_analytics_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-internal title: "@kbn/core-analytics-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-internal'] --- import kbnCoreAnalyticsBrowserInternalObj from './kbn_core_analytics_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_mocks.mdx b/api_docs/kbn_core_analytics_browser_mocks.mdx index 96683f7c57013..36e56a6b1c924 100644 --- a/api_docs/kbn_core_analytics_browser_mocks.mdx +++ b/api_docs/kbn_core_analytics_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-mocks title: "@kbn/core-analytics-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-mocks'] --- import kbnCoreAnalyticsBrowserMocksObj from './kbn_core_analytics_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server.mdx b/api_docs/kbn_core_analytics_server.mdx index 0cd5b193f2df7..b7fde8569297b 100644 --- a/api_docs/kbn_core_analytics_server.mdx +++ b/api_docs/kbn_core_analytics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server title: "@kbn/core-analytics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server'] --- import kbnCoreAnalyticsServerObj from './kbn_core_analytics_server.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_internal.mdx b/api_docs/kbn_core_analytics_server_internal.mdx index d04d60b4104f2..7e47fbd263004 100644 --- a/api_docs/kbn_core_analytics_server_internal.mdx +++ b/api_docs/kbn_core_analytics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-internal title: "@kbn/core-analytics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-internal'] --- import kbnCoreAnalyticsServerInternalObj from './kbn_core_analytics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_mocks.mdx b/api_docs/kbn_core_analytics_server_mocks.mdx index c8627bcdddcad..68c026524cf9a 100644 --- a/api_docs/kbn_core_analytics_server_mocks.mdx +++ b/api_docs/kbn_core_analytics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-mocks title: "@kbn/core-analytics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-mocks'] --- import kbnCoreAnalyticsServerMocksObj from './kbn_core_analytics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser.mdx b/api_docs/kbn_core_application_browser.mdx index 4a993213a6b8d..7b53381c16d63 100644 --- a/api_docs/kbn_core_application_browser.mdx +++ b/api_docs/kbn_core_application_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser title: "@kbn/core-application-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser'] --- import kbnCoreApplicationBrowserObj from './kbn_core_application_browser.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_internal.mdx b/api_docs/kbn_core_application_browser_internal.mdx index eecaae2a75c71..62e84f8b51e19 100644 --- a/api_docs/kbn_core_application_browser_internal.mdx +++ b/api_docs/kbn_core_application_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-internal title: "@kbn/core-application-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-internal'] --- import kbnCoreApplicationBrowserInternalObj from './kbn_core_application_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_mocks.mdx b/api_docs/kbn_core_application_browser_mocks.mdx index 6735afdc39bbd..4053b0a55fe5a 100644 --- a/api_docs/kbn_core_application_browser_mocks.mdx +++ b/api_docs/kbn_core_application_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-mocks title: "@kbn/core-application-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-mocks'] --- import kbnCoreApplicationBrowserMocksObj from './kbn_core_application_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_common.mdx b/api_docs/kbn_core_application_common.mdx index c6e54877757c5..bff49ec7bd176 100644 --- a/api_docs/kbn_core_application_common.mdx +++ b/api_docs/kbn_core_application_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-common title: "@kbn/core-application-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-common plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-common'] --- import kbnCoreApplicationCommonObj from './kbn_core_application_common.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_internal.mdx b/api_docs/kbn_core_apps_browser_internal.mdx index 8e570ae9eec80..65575ae5ef538 100644 --- a/api_docs/kbn_core_apps_browser_internal.mdx +++ b/api_docs/kbn_core_apps_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-internal title: "@kbn/core-apps-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-internal'] --- import kbnCoreAppsBrowserInternalObj from './kbn_core_apps_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_mocks.mdx b/api_docs/kbn_core_apps_browser_mocks.mdx index f84073a754021..c6777ea9abe31 100644 --- a/api_docs/kbn_core_apps_browser_mocks.mdx +++ b/api_docs/kbn_core_apps_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-mocks title: "@kbn/core-apps-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-mocks'] --- import kbnCoreAppsBrowserMocksObj from './kbn_core_apps_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_apps_server_internal.mdx b/api_docs/kbn_core_apps_server_internal.mdx index b92ff5eca4747..d5542c4a24ff2 100644 --- a/api_docs/kbn_core_apps_server_internal.mdx +++ b/api_docs/kbn_core_apps_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-server-internal title: "@kbn/core-apps-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-server-internal'] --- import kbnCoreAppsServerInternalObj from './kbn_core_apps_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_browser_mocks.mdx b/api_docs/kbn_core_base_browser_mocks.mdx index 543ed49186acc..9482a704e2ee9 100644 --- a/api_docs/kbn_core_base_browser_mocks.mdx +++ b/api_docs/kbn_core_base_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-browser-mocks title: "@kbn/core-base-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-browser-mocks'] --- import kbnCoreBaseBrowserMocksObj from './kbn_core_base_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_base_common.mdx b/api_docs/kbn_core_base_common.mdx index 1a1d14632e577..479365fa00e3c 100644 --- a/api_docs/kbn_core_base_common.mdx +++ b/api_docs/kbn_core_base_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-common title: "@kbn/core-base-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-common plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-common'] --- import kbnCoreBaseCommonObj from './kbn_core_base_common.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_internal.mdx b/api_docs/kbn_core_base_server_internal.mdx index 46a6e8b06a16c..b592f484ad8fc 100644 --- a/api_docs/kbn_core_base_server_internal.mdx +++ b/api_docs/kbn_core_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-internal title: "@kbn/core-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-internal'] --- import kbnCoreBaseServerInternalObj from './kbn_core_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_mocks.mdx b/api_docs/kbn_core_base_server_mocks.mdx index 86d2ede64224d..c63d9551eb122 100644 --- a/api_docs/kbn_core_base_server_mocks.mdx +++ b/api_docs/kbn_core_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-mocks title: "@kbn/core-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-mocks'] --- import kbnCoreBaseServerMocksObj from './kbn_core_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_browser_mocks.mdx b/api_docs/kbn_core_capabilities_browser_mocks.mdx index 3c4f3da18512d..f764ace99b39b 100644 --- a/api_docs/kbn_core_capabilities_browser_mocks.mdx +++ b/api_docs/kbn_core_capabilities_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-browser-mocks title: "@kbn/core-capabilities-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-browser-mocks'] --- import kbnCoreCapabilitiesBrowserMocksObj from './kbn_core_capabilities_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_common.mdx b/api_docs/kbn_core_capabilities_common.mdx index bb8500abcd53e..090f79705cd6a 100644 --- a/api_docs/kbn_core_capabilities_common.mdx +++ b/api_docs/kbn_core_capabilities_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-common title: "@kbn/core-capabilities-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-common plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-common'] --- import kbnCoreCapabilitiesCommonObj from './kbn_core_capabilities_common.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server.mdx b/api_docs/kbn_core_capabilities_server.mdx index 345b7519c0c90..0270a944be372 100644 --- a/api_docs/kbn_core_capabilities_server.mdx +++ b/api_docs/kbn_core_capabilities_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server title: "@kbn/core-capabilities-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server'] --- import kbnCoreCapabilitiesServerObj from './kbn_core_capabilities_server.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server_mocks.mdx b/api_docs/kbn_core_capabilities_server_mocks.mdx index 23c1cc1b06d5a..3ce4665c32e47 100644 --- a/api_docs/kbn_core_capabilities_server_mocks.mdx +++ b/api_docs/kbn_core_capabilities_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server-mocks title: "@kbn/core-capabilities-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server-mocks'] --- import kbnCoreCapabilitiesServerMocksObj from './kbn_core_capabilities_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser.mdx b/api_docs/kbn_core_chrome_browser.mdx index 08c57ef7827b8..01eb858c31f0b 100644 --- a/api_docs/kbn_core_chrome_browser.mdx +++ b/api_docs/kbn_core_chrome_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser title: "@kbn/core-chrome-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser'] --- import kbnCoreChromeBrowserObj from './kbn_core_chrome_browser.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser_mocks.mdx b/api_docs/kbn_core_chrome_browser_mocks.mdx index 384aec1c07acc..dee0a00e0cdb6 100644 --- a/api_docs/kbn_core_chrome_browser_mocks.mdx +++ b/api_docs/kbn_core_chrome_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser-mocks title: "@kbn/core-chrome-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser-mocks'] --- import kbnCoreChromeBrowserMocksObj from './kbn_core_chrome_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_config_server_internal.mdx b/api_docs/kbn_core_config_server_internal.mdx index 2dbcfb76b16c0..ddb303ac70cd5 100644 --- a/api_docs/kbn_core_config_server_internal.mdx +++ b/api_docs/kbn_core_config_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-config-server-internal title: "@kbn/core-config-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-config-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-config-server-internal'] --- import kbnCoreConfigServerInternalObj from './kbn_core_config_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser.mdx b/api_docs/kbn_core_deprecations_browser.mdx index cf72cc2da6a8c..22ed289f6e797 100644 --- a/api_docs/kbn_core_deprecations_browser.mdx +++ b/api_docs/kbn_core_deprecations_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser title: "@kbn/core-deprecations-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser'] --- import kbnCoreDeprecationsBrowserObj from './kbn_core_deprecations_browser.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_internal.mdx b/api_docs/kbn_core_deprecations_browser_internal.mdx index d1d52e3121925..5358dd3913124 100644 --- a/api_docs/kbn_core_deprecations_browser_internal.mdx +++ b/api_docs/kbn_core_deprecations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-internal title: "@kbn/core-deprecations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-internal'] --- import kbnCoreDeprecationsBrowserInternalObj from './kbn_core_deprecations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_mocks.mdx b/api_docs/kbn_core_deprecations_browser_mocks.mdx index fee1ebaf16c52..c46cd772ea0c4 100644 --- a/api_docs/kbn_core_deprecations_browser_mocks.mdx +++ b/api_docs/kbn_core_deprecations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-mocks title: "@kbn/core-deprecations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-mocks'] --- import kbnCoreDeprecationsBrowserMocksObj from './kbn_core_deprecations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_common.mdx b/api_docs/kbn_core_deprecations_common.mdx index b3be2ff3190a5..cfdeff6a4e6d1 100644 --- a/api_docs/kbn_core_deprecations_common.mdx +++ b/api_docs/kbn_core_deprecations_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-common title: "@kbn/core-deprecations-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-common plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-common'] --- import kbnCoreDeprecationsCommonObj from './kbn_core_deprecations_common.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server.mdx b/api_docs/kbn_core_deprecations_server.mdx index 4292e02b240d8..83faca19078fd 100644 --- a/api_docs/kbn_core_deprecations_server.mdx +++ b/api_docs/kbn_core_deprecations_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server title: "@kbn/core-deprecations-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server'] --- import kbnCoreDeprecationsServerObj from './kbn_core_deprecations_server.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_internal.mdx b/api_docs/kbn_core_deprecations_server_internal.mdx index 326cc39006187..2ba93dbf708e9 100644 --- a/api_docs/kbn_core_deprecations_server_internal.mdx +++ b/api_docs/kbn_core_deprecations_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-internal title: "@kbn/core-deprecations-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-internal'] --- import kbnCoreDeprecationsServerInternalObj from './kbn_core_deprecations_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_mocks.mdx b/api_docs/kbn_core_deprecations_server_mocks.mdx index 27c82ae4554a6..9deccddddbadc 100644 --- a/api_docs/kbn_core_deprecations_server_mocks.mdx +++ b/api_docs/kbn_core_deprecations_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-mocks title: "@kbn/core-deprecations-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-mocks'] --- import kbnCoreDeprecationsServerMocksObj from './kbn_core_deprecations_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser.mdx b/api_docs/kbn_core_doc_links_browser.mdx index 41976eabc63f5..2b0c250aa1c90 100644 --- a/api_docs/kbn_core_doc_links_browser.mdx +++ b/api_docs/kbn_core_doc_links_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser title: "@kbn/core-doc-links-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser'] --- import kbnCoreDocLinksBrowserObj from './kbn_core_doc_links_browser.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser_mocks.mdx b/api_docs/kbn_core_doc_links_browser_mocks.mdx index acee56307cd6d..613833c8bc6d8 100644 --- a/api_docs/kbn_core_doc_links_browser_mocks.mdx +++ b/api_docs/kbn_core_doc_links_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser-mocks title: "@kbn/core-doc-links-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser-mocks'] --- import kbnCoreDocLinksBrowserMocksObj from './kbn_core_doc_links_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server.mdx b/api_docs/kbn_core_doc_links_server.mdx index 94c46ed3ba9f2..e44f21c1869d0 100644 --- a/api_docs/kbn_core_doc_links_server.mdx +++ b/api_docs/kbn_core_doc_links_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server title: "@kbn/core-doc-links-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server'] --- import kbnCoreDocLinksServerObj from './kbn_core_doc_links_server.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server_mocks.mdx b/api_docs/kbn_core_doc_links_server_mocks.mdx index b4f90ff3d7697..2bcecf050d91e 100644 --- a/api_docs/kbn_core_doc_links_server_mocks.mdx +++ b/api_docs/kbn_core_doc_links_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server-mocks title: "@kbn/core-doc-links-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server-mocks'] --- import kbnCoreDocLinksServerMocksObj from './kbn_core_doc_links_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx index 9fc552d295350..1ca9fa269729d 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-internal title: "@kbn/core-elasticsearch-client-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-internal'] --- import kbnCoreElasticsearchClientServerInternalObj from './kbn_core_elasticsearch_client_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx index 723de8e8a81aa..2a2e83bf226c3 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-mocks title: "@kbn/core-elasticsearch-client-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-mocks'] --- import kbnCoreElasticsearchClientServerMocksObj from './kbn_core_elasticsearch_client_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server.mdx b/api_docs/kbn_core_elasticsearch_server.mdx index 8fa3caf66527a..80ef7fa899a73 100644 --- a/api_docs/kbn_core_elasticsearch_server.mdx +++ b/api_docs/kbn_core_elasticsearch_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server title: "@kbn/core-elasticsearch-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server'] --- import kbnCoreElasticsearchServerObj from './kbn_core_elasticsearch_server.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_internal.mdx b/api_docs/kbn_core_elasticsearch_server_internal.mdx index f8c012aebbbc1..612c3fdc80ff6 100644 --- a/api_docs/kbn_core_elasticsearch_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-internal title: "@kbn/core-elasticsearch-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-internal'] --- import kbnCoreElasticsearchServerInternalObj from './kbn_core_elasticsearch_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_server_mocks.mdx index 8215ec1868aa6..15d6800dd8848 100644 --- a/api_docs/kbn_core_elasticsearch_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-mocks title: "@kbn/core-elasticsearch-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-mocks'] --- import kbnCoreElasticsearchServerMocksObj from './kbn_core_elasticsearch_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_internal.mdx b/api_docs/kbn_core_environment_server_internal.mdx index 25659141fd964..ef6b027c85a46 100644 --- a/api_docs/kbn_core_environment_server_internal.mdx +++ b/api_docs/kbn_core_environment_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-internal title: "@kbn/core-environment-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-internal'] --- import kbnCoreEnvironmentServerInternalObj from './kbn_core_environment_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_mocks.mdx b/api_docs/kbn_core_environment_server_mocks.mdx index 7c16b1b88d0be..a66ecb0ae138a 100644 --- a/api_docs/kbn_core_environment_server_mocks.mdx +++ b/api_docs/kbn_core_environment_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-mocks title: "@kbn/core-environment-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-mocks'] --- import kbnCoreEnvironmentServerMocksObj from './kbn_core_environment_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser.mdx b/api_docs/kbn_core_execution_context_browser.mdx index c3e4bc90bcd6b..4a6f867acc77c 100644 --- a/api_docs/kbn_core_execution_context_browser.mdx +++ b/api_docs/kbn_core_execution_context_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser title: "@kbn/core-execution-context-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser'] --- import kbnCoreExecutionContextBrowserObj from './kbn_core_execution_context_browser.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_internal.mdx b/api_docs/kbn_core_execution_context_browser_internal.mdx index 74bfb732cfa0d..ed88cc5d2a76d 100644 --- a/api_docs/kbn_core_execution_context_browser_internal.mdx +++ b/api_docs/kbn_core_execution_context_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-internal title: "@kbn/core-execution-context-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-internal'] --- import kbnCoreExecutionContextBrowserInternalObj from './kbn_core_execution_context_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_mocks.mdx b/api_docs/kbn_core_execution_context_browser_mocks.mdx index 3a3bde1ee7d53..383f46d4e15da 100644 --- a/api_docs/kbn_core_execution_context_browser_mocks.mdx +++ b/api_docs/kbn_core_execution_context_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-mocks title: "@kbn/core-execution-context-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-mocks'] --- import kbnCoreExecutionContextBrowserMocksObj from './kbn_core_execution_context_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_common.mdx b/api_docs/kbn_core_execution_context_common.mdx index c767ec003d6c6..1db0c8a636715 100644 --- a/api_docs/kbn_core_execution_context_common.mdx +++ b/api_docs/kbn_core_execution_context_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-common title: "@kbn/core-execution-context-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-common plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-common'] --- import kbnCoreExecutionContextCommonObj from './kbn_core_execution_context_common.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server.mdx b/api_docs/kbn_core_execution_context_server.mdx index 91fadda5fa8c1..4c7474451e713 100644 --- a/api_docs/kbn_core_execution_context_server.mdx +++ b/api_docs/kbn_core_execution_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server title: "@kbn/core-execution-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server'] --- import kbnCoreExecutionContextServerObj from './kbn_core_execution_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_internal.mdx b/api_docs/kbn_core_execution_context_server_internal.mdx index 123caba955169..5730725070d13 100644 --- a/api_docs/kbn_core_execution_context_server_internal.mdx +++ b/api_docs/kbn_core_execution_context_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-internal title: "@kbn/core-execution-context-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-internal'] --- import kbnCoreExecutionContextServerInternalObj from './kbn_core_execution_context_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_mocks.mdx b/api_docs/kbn_core_execution_context_server_mocks.mdx index d2c079445d1f4..071f8b7068ba2 100644 --- a/api_docs/kbn_core_execution_context_server_mocks.mdx +++ b/api_docs/kbn_core_execution_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-mocks title: "@kbn/core-execution-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-mocks'] --- import kbnCoreExecutionContextServerMocksObj from './kbn_core_execution_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser.mdx b/api_docs/kbn_core_fatal_errors_browser.mdx index b95d4f4045da2..b0fa24033f4a6 100644 --- a/api_docs/kbn_core_fatal_errors_browser.mdx +++ b/api_docs/kbn_core_fatal_errors_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser title: "@kbn/core-fatal-errors-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser'] --- import kbnCoreFatalErrorsBrowserObj from './kbn_core_fatal_errors_browser.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx index 5fc132c687107..5acf48e6d4181 100644 --- a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx +++ b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser-mocks title: "@kbn/core-fatal-errors-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser-mocks'] --- import kbnCoreFatalErrorsBrowserMocksObj from './kbn_core_fatal_errors_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser.mdx b/api_docs/kbn_core_http_browser.mdx index fe2c7ad8921f8..31225f7097a6d 100644 --- a/api_docs/kbn_core_http_browser.mdx +++ b/api_docs/kbn_core_http_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser title: "@kbn/core-http-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser'] --- import kbnCoreHttpBrowserObj from './kbn_core_http_browser.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_internal.mdx b/api_docs/kbn_core_http_browser_internal.mdx index cff8db8b7d581..59df5d002928a 100644 --- a/api_docs/kbn_core_http_browser_internal.mdx +++ b/api_docs/kbn_core_http_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-internal title: "@kbn/core-http-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-internal'] --- import kbnCoreHttpBrowserInternalObj from './kbn_core_http_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_mocks.mdx b/api_docs/kbn_core_http_browser_mocks.mdx index 29b4791b2c8ac..06a27e846551e 100644 --- a/api_docs/kbn_core_http_browser_mocks.mdx +++ b/api_docs/kbn_core_http_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-mocks title: "@kbn/core-http-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-mocks'] --- import kbnCoreHttpBrowserMocksObj from './kbn_core_http_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_common.mdx b/api_docs/kbn_core_http_common.mdx index 5e1c8fa7b1fcf..6ee0704747ccf 100644 --- a/api_docs/kbn_core_http_common.mdx +++ b/api_docs/kbn_core_http_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-common title: "@kbn/core-http-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-common plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-common'] --- import kbnCoreHttpCommonObj from './kbn_core_http_common.devdocs.json'; diff --git a/api_docs/kbn_core_http_context_server_mocks.mdx b/api_docs/kbn_core_http_context_server_mocks.mdx index b8df949947cc5..e78354a06a235 100644 --- a/api_docs/kbn_core_http_context_server_mocks.mdx +++ b/api_docs/kbn_core_http_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-context-server-mocks title: "@kbn/core-http-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-context-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-context-server-mocks'] --- import kbnCoreHttpContextServerMocksObj from './kbn_core_http_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_request_handler_context_server.mdx b/api_docs/kbn_core_http_request_handler_context_server.mdx index 01b5432f9b2ae..e7aec9fa6a342 100644 --- a/api_docs/kbn_core_http_request_handler_context_server.mdx +++ b/api_docs/kbn_core_http_request_handler_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-request-handler-context-server title: "@kbn/core-http-request-handler-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-request-handler-context-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-request-handler-context-server'] --- import kbnCoreHttpRequestHandlerContextServerObj from './kbn_core_http_request_handler_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server.mdx b/api_docs/kbn_core_http_resources_server.mdx index fb5c8f6ecda5e..9a638e75a288d 100644 --- a/api_docs/kbn_core_http_resources_server.mdx +++ b/api_docs/kbn_core_http_resources_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server title: "@kbn/core-http-resources-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server'] --- import kbnCoreHttpResourcesServerObj from './kbn_core_http_resources_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_internal.mdx b/api_docs/kbn_core_http_resources_server_internal.mdx index c05dc0186bcd4..4d9a3bbad83df 100644 --- a/api_docs/kbn_core_http_resources_server_internal.mdx +++ b/api_docs/kbn_core_http_resources_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-internal title: "@kbn/core-http-resources-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-internal'] --- import kbnCoreHttpResourcesServerInternalObj from './kbn_core_http_resources_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_mocks.mdx b/api_docs/kbn_core_http_resources_server_mocks.mdx index 715da6905354d..5b7620d4edb80 100644 --- a/api_docs/kbn_core_http_resources_server_mocks.mdx +++ b/api_docs/kbn_core_http_resources_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-mocks title: "@kbn/core-http-resources-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-mocks'] --- import kbnCoreHttpResourcesServerMocksObj from './kbn_core_http_resources_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_internal.mdx b/api_docs/kbn_core_http_router_server_internal.mdx index 2a2abd4584c98..735e2757d6a3f 100644 --- a/api_docs/kbn_core_http_router_server_internal.mdx +++ b/api_docs/kbn_core_http_router_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-internal title: "@kbn/core-http-router-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-internal'] --- import kbnCoreHttpRouterServerInternalObj from './kbn_core_http_router_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_mocks.mdx b/api_docs/kbn_core_http_router_server_mocks.mdx index 4d9136b928e34..edf48b97c4a63 100644 --- a/api_docs/kbn_core_http_router_server_mocks.mdx +++ b/api_docs/kbn_core_http_router_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-mocks title: "@kbn/core-http-router-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-mocks'] --- import kbnCoreHttpRouterServerMocksObj from './kbn_core_http_router_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_server.devdocs.json b/api_docs/kbn_core_http_server.devdocs.json index 54f2b180ee848..67d50befff8cf 100644 --- a/api_docs/kbn_core_http_server.devdocs.json +++ b/api_docs/kbn_core_http_server.devdocs.json @@ -2665,22 +2665,6 @@ "path": "packages/core/http/core-http-server/src/router/socket.ts", "deprecated": false, "trackAdoption": false - }, - { - "parentPluginId": "@kbn/core-http-server", - "id": "def-server.IKibanaSocket.remoteAddress", - "type": "string", - "tags": [], - "label": "remoteAddress", - "description": [ - "\nThe string representation of the remote IP address. For example,`'74.125.127.100'` or\n`'2001:4860:a005::68'`. Value may be `undefined` if the socket is destroyed (for example, if\nthe client disconnected)." - ], - "signature": [ - "string | undefined" - ], - "path": "packages/core/http/core-http-server/src/router/socket.ts", - "deprecated": false, - "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/kbn_core_http_server.mdx b/api_docs/kbn_core_http_server.mdx index b75f7749b9fc8..720617c3f4978 100644 --- a/api_docs/kbn_core_http_server.mdx +++ b/api_docs/kbn_core_http_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server title: "@kbn/core-http-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server'] --- import kbnCoreHttpServerObj from './kbn_core_http_server.devdocs.json'; @@ -21,7 +21,7 @@ Contact Kibana Core for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 393 | 1 | 154 | 0 | +| 392 | 1 | 154 | 0 | ## Server diff --git a/api_docs/kbn_core_http_server_internal.mdx b/api_docs/kbn_core_http_server_internal.mdx index c8764c3f90809..2deba504e4f45 100644 --- a/api_docs/kbn_core_http_server_internal.mdx +++ b/api_docs/kbn_core_http_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-internal title: "@kbn/core-http-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-internal'] --- import kbnCoreHttpServerInternalObj from './kbn_core_http_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_mocks.mdx b/api_docs/kbn_core_http_server_mocks.mdx index 52c54c9238ecd..5e426e4317f8a 100644 --- a/api_docs/kbn_core_http_server_mocks.mdx +++ b/api_docs/kbn_core_http_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-mocks title: "@kbn/core-http-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-mocks'] --- import kbnCoreHttpServerMocksObj from './kbn_core_http_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser.mdx b/api_docs/kbn_core_i18n_browser.mdx index b444d8beedcca..cc9761c6bb7ac 100644 --- a/api_docs/kbn_core_i18n_browser.mdx +++ b/api_docs/kbn_core_i18n_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser title: "@kbn/core-i18n-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser'] --- import kbnCoreI18nBrowserObj from './kbn_core_i18n_browser.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser_mocks.mdx b/api_docs/kbn_core_i18n_browser_mocks.mdx index 1f7db30f4378b..934dbd2f3595a 100644 --- a/api_docs/kbn_core_i18n_browser_mocks.mdx +++ b/api_docs/kbn_core_i18n_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser-mocks title: "@kbn/core-i18n-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser-mocks'] --- import kbnCoreI18nBrowserMocksObj from './kbn_core_i18n_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server.mdx b/api_docs/kbn_core_i18n_server.mdx index d43fd1317bf57..1a2891900b37e 100644 --- a/api_docs/kbn_core_i18n_server.mdx +++ b/api_docs/kbn_core_i18n_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server title: "@kbn/core-i18n-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server'] --- import kbnCoreI18nServerObj from './kbn_core_i18n_server.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_internal.mdx b/api_docs/kbn_core_i18n_server_internal.mdx index e756009ae883e..347025347cabf 100644 --- a/api_docs/kbn_core_i18n_server_internal.mdx +++ b/api_docs/kbn_core_i18n_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-internal title: "@kbn/core-i18n-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-internal'] --- import kbnCoreI18nServerInternalObj from './kbn_core_i18n_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_mocks.mdx b/api_docs/kbn_core_i18n_server_mocks.mdx index 6ee8acdb7557e..88b8e40017542 100644 --- a/api_docs/kbn_core_i18n_server_mocks.mdx +++ b/api_docs/kbn_core_i18n_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-mocks title: "@kbn/core-i18n-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-mocks'] --- import kbnCoreI18nServerMocksObj from './kbn_core_i18n_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_injected_metadata_browser.mdx b/api_docs/kbn_core_injected_metadata_browser.mdx index 70a5c808f2375..0f1b26f68e4d4 100644 --- a/api_docs/kbn_core_injected_metadata_browser.mdx +++ b/api_docs/kbn_core_injected_metadata_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser title: "@kbn/core-injected-metadata-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-injected-metadata-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser'] --- import kbnCoreInjectedMetadataBrowserObj from './kbn_core_injected_metadata_browser.devdocs.json'; diff --git a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx index c7f2c060777aa..0b11ff27184ba 100644 --- a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx +++ b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser-mocks title: "@kbn/core-injected-metadata-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-injected-metadata-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser-mocks'] --- import kbnCoreInjectedMetadataBrowserMocksObj from './kbn_core_injected_metadata_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_internal.mdx b/api_docs/kbn_core_integrations_browser_internal.mdx index e1b66d01654b0..f3efe634c03ed 100644 --- a/api_docs/kbn_core_integrations_browser_internal.mdx +++ b/api_docs/kbn_core_integrations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-internal title: "@kbn/core-integrations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-internal'] --- import kbnCoreIntegrationsBrowserInternalObj from './kbn_core_integrations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_mocks.mdx b/api_docs/kbn_core_integrations_browser_mocks.mdx index e1f41dabe99be..365190d5e666e 100644 --- a/api_docs/kbn_core_integrations_browser_mocks.mdx +++ b/api_docs/kbn_core_integrations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-mocks title: "@kbn/core-integrations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-mocks'] --- import kbnCoreIntegrationsBrowserMocksObj from './kbn_core_integrations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser.mdx b/api_docs/kbn_core_lifecycle_browser.mdx index 202cf93826869..2f5c54c5c1911 100644 --- a/api_docs/kbn_core_lifecycle_browser.mdx +++ b/api_docs/kbn_core_lifecycle_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser title: "@kbn/core-lifecycle-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser'] --- import kbnCoreLifecycleBrowserObj from './kbn_core_lifecycle_browser.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser_mocks.mdx b/api_docs/kbn_core_lifecycle_browser_mocks.mdx index 42e6b8934c240..7fcca2b093060 100644 --- a/api_docs/kbn_core_lifecycle_browser_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser-mocks title: "@kbn/core-lifecycle-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser-mocks'] --- import kbnCoreLifecycleBrowserMocksObj from './kbn_core_lifecycle_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server.mdx b/api_docs/kbn_core_lifecycle_server.mdx index 672040caa0499..527977de855e7 100644 --- a/api_docs/kbn_core_lifecycle_server.mdx +++ b/api_docs/kbn_core_lifecycle_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server title: "@kbn/core-lifecycle-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server'] --- import kbnCoreLifecycleServerObj from './kbn_core_lifecycle_server.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server_mocks.mdx b/api_docs/kbn_core_lifecycle_server_mocks.mdx index a8901afd4a5c7..2fd39a7a81193 100644 --- a/api_docs/kbn_core_lifecycle_server_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server-mocks title: "@kbn/core-lifecycle-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server-mocks'] --- import kbnCoreLifecycleServerMocksObj from './kbn_core_lifecycle_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_browser_mocks.mdx b/api_docs/kbn_core_logging_browser_mocks.mdx index 2cbe09170cc78..0c2572f7ca840 100644 --- a/api_docs/kbn_core_logging_browser_mocks.mdx +++ b/api_docs/kbn_core_logging_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-browser-mocks title: "@kbn/core-logging-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-browser-mocks'] --- import kbnCoreLoggingBrowserMocksObj from './kbn_core_logging_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_common_internal.mdx b/api_docs/kbn_core_logging_common_internal.mdx index d4fbf54d20904..29ad4f8ae92be 100644 --- a/api_docs/kbn_core_logging_common_internal.mdx +++ b/api_docs/kbn_core_logging_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-common-internal title: "@kbn/core-logging-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-common-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-common-internal'] --- import kbnCoreLoggingCommonInternalObj from './kbn_core_logging_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server.mdx b/api_docs/kbn_core_logging_server.mdx index 26de41f176295..0940417b20d44 100644 --- a/api_docs/kbn_core_logging_server.mdx +++ b/api_docs/kbn_core_logging_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server title: "@kbn/core-logging-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server'] --- import kbnCoreLoggingServerObj from './kbn_core_logging_server.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_internal.mdx b/api_docs/kbn_core_logging_server_internal.mdx index 517e66e88b75e..c0feddc1f6a3e 100644 --- a/api_docs/kbn_core_logging_server_internal.mdx +++ b/api_docs/kbn_core_logging_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-internal title: "@kbn/core-logging-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-internal'] --- import kbnCoreLoggingServerInternalObj from './kbn_core_logging_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_mocks.mdx b/api_docs/kbn_core_logging_server_mocks.mdx index 3c08d03d94c83..432115756a6e3 100644 --- a/api_docs/kbn_core_logging_server_mocks.mdx +++ b/api_docs/kbn_core_logging_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-mocks title: "@kbn/core-logging-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-mocks'] --- import kbnCoreLoggingServerMocksObj from './kbn_core_logging_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_internal.mdx b/api_docs/kbn_core_metrics_collectors_server_internal.mdx index 48624f02556b2..17591a5067140 100644 --- a/api_docs/kbn_core_metrics_collectors_server_internal.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-internal title: "@kbn/core-metrics-collectors-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-internal'] --- import kbnCoreMetricsCollectorsServerInternalObj from './kbn_core_metrics_collectors_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx index c45d44df6c7d6..839d82402a0ac 100644 --- a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-mocks title: "@kbn/core-metrics-collectors-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-mocks'] --- import kbnCoreMetricsCollectorsServerMocksObj from './kbn_core_metrics_collectors_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server.mdx b/api_docs/kbn_core_metrics_server.mdx index 496e7cabc4b6e..835eb873ca943 100644 --- a/api_docs/kbn_core_metrics_server.mdx +++ b/api_docs/kbn_core_metrics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server title: "@kbn/core-metrics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server'] --- import kbnCoreMetricsServerObj from './kbn_core_metrics_server.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_internal.mdx b/api_docs/kbn_core_metrics_server_internal.mdx index 2733ab2d6abd7..49d0d5a2e8633 100644 --- a/api_docs/kbn_core_metrics_server_internal.mdx +++ b/api_docs/kbn_core_metrics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-internal title: "@kbn/core-metrics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-internal'] --- import kbnCoreMetricsServerInternalObj from './kbn_core_metrics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_mocks.mdx b/api_docs/kbn_core_metrics_server_mocks.mdx index e04ccc2485178..96b5df6e5b3f0 100644 --- a/api_docs/kbn_core_metrics_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-mocks title: "@kbn/core-metrics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-mocks'] --- import kbnCoreMetricsServerMocksObj from './kbn_core_metrics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_mount_utils_browser.mdx b/api_docs/kbn_core_mount_utils_browser.mdx index d0343bd586ac3..09733d8e21a2f 100644 --- a/api_docs/kbn_core_mount_utils_browser.mdx +++ b/api_docs/kbn_core_mount_utils_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-mount-utils-browser title: "@kbn/core-mount-utils-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-mount-utils-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-mount-utils-browser'] --- import kbnCoreMountUtilsBrowserObj from './kbn_core_mount_utils_browser.devdocs.json'; diff --git a/api_docs/kbn_core_node_server.mdx b/api_docs/kbn_core_node_server.mdx index a15fecedbc1fc..6886db770d962 100644 --- a/api_docs/kbn_core_node_server.mdx +++ b/api_docs/kbn_core_node_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server title: "@kbn/core-node-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server'] --- import kbnCoreNodeServerObj from './kbn_core_node_server.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_internal.mdx b/api_docs/kbn_core_node_server_internal.mdx index 3b9825e23530e..fdcbb68ccd853 100644 --- a/api_docs/kbn_core_node_server_internal.mdx +++ b/api_docs/kbn_core_node_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-internal title: "@kbn/core-node-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-internal'] --- import kbnCoreNodeServerInternalObj from './kbn_core_node_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_mocks.mdx b/api_docs/kbn_core_node_server_mocks.mdx index 068ea73beb5c8..747a2bc90d0e1 100644 --- a/api_docs/kbn_core_node_server_mocks.mdx +++ b/api_docs/kbn_core_node_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-mocks title: "@kbn/core-node-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-mocks'] --- import kbnCoreNodeServerMocksObj from './kbn_core_node_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser.mdx b/api_docs/kbn_core_notifications_browser.mdx index 74a9cb5533e94..2a4d6433fdb2a 100644 --- a/api_docs/kbn_core_notifications_browser.mdx +++ b/api_docs/kbn_core_notifications_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser title: "@kbn/core-notifications-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser'] --- import kbnCoreNotificationsBrowserObj from './kbn_core_notifications_browser.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_internal.mdx b/api_docs/kbn_core_notifications_browser_internal.mdx index 55ec31ef08a4d..ff6836c757b75 100644 --- a/api_docs/kbn_core_notifications_browser_internal.mdx +++ b/api_docs/kbn_core_notifications_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-internal title: "@kbn/core-notifications-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-internal'] --- import kbnCoreNotificationsBrowserInternalObj from './kbn_core_notifications_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_mocks.mdx b/api_docs/kbn_core_notifications_browser_mocks.mdx index d5cce1c4a9de1..9454e5bccc4c2 100644 --- a/api_docs/kbn_core_notifications_browser_mocks.mdx +++ b/api_docs/kbn_core_notifications_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-mocks title: "@kbn/core-notifications-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-mocks'] --- import kbnCoreNotificationsBrowserMocksObj from './kbn_core_notifications_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser.mdx b/api_docs/kbn_core_overlays_browser.mdx index aefe154a497de..bcb5dce572d8c 100644 --- a/api_docs/kbn_core_overlays_browser.mdx +++ b/api_docs/kbn_core_overlays_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser title: "@kbn/core-overlays-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser'] --- import kbnCoreOverlaysBrowserObj from './kbn_core_overlays_browser.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_internal.mdx b/api_docs/kbn_core_overlays_browser_internal.mdx index 11c46d08e672f..5f444bda3c9d5 100644 --- a/api_docs/kbn_core_overlays_browser_internal.mdx +++ b/api_docs/kbn_core_overlays_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-internal title: "@kbn/core-overlays-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-internal'] --- import kbnCoreOverlaysBrowserInternalObj from './kbn_core_overlays_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_mocks.mdx b/api_docs/kbn_core_overlays_browser_mocks.mdx index 5e2f048c75ac6..7a5682facdf84 100644 --- a/api_docs/kbn_core_overlays_browser_mocks.mdx +++ b/api_docs/kbn_core_overlays_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-mocks title: "@kbn/core-overlays-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-mocks'] --- import kbnCoreOverlaysBrowserMocksObj from './kbn_core_overlays_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser.mdx b/api_docs/kbn_core_plugins_browser.mdx index 3bd28ecd1eaab..a34511073e2a2 100644 --- a/api_docs/kbn_core_plugins_browser.mdx +++ b/api_docs/kbn_core_plugins_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser title: "@kbn/core-plugins-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser'] --- import kbnCorePluginsBrowserObj from './kbn_core_plugins_browser.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser_mocks.mdx b/api_docs/kbn_core_plugins_browser_mocks.mdx index 97ee8cdba4052..8c2e1216a3c10 100644 --- a/api_docs/kbn_core_plugins_browser_mocks.mdx +++ b/api_docs/kbn_core_plugins_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser-mocks title: "@kbn/core-plugins-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser-mocks'] --- import kbnCorePluginsBrowserMocksObj from './kbn_core_plugins_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_server.mdx b/api_docs/kbn_core_plugins_server.mdx index 2ccd337a0d049..b483dfd33df50 100644 --- a/api_docs/kbn_core_plugins_server.mdx +++ b/api_docs/kbn_core_plugins_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server title: "@kbn/core-plugins-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server'] --- import kbnCorePluginsServerObj from './kbn_core_plugins_server.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_server_mocks.mdx b/api_docs/kbn_core_plugins_server_mocks.mdx index e271d309f5938..2ead5341c27f3 100644 --- a/api_docs/kbn_core_plugins_server_mocks.mdx +++ b/api_docs/kbn_core_plugins_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server-mocks title: "@kbn/core-plugins-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server-mocks'] --- import kbnCorePluginsServerMocksObj from './kbn_core_plugins_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server.mdx b/api_docs/kbn_core_preboot_server.mdx index a3c42026043ce..a5a3bbd968d67 100644 --- a/api_docs/kbn_core_preboot_server.mdx +++ b/api_docs/kbn_core_preboot_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server title: "@kbn/core-preboot-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server'] --- import kbnCorePrebootServerObj from './kbn_core_preboot_server.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server_mocks.mdx b/api_docs/kbn_core_preboot_server_mocks.mdx index 57656a23ba32d..3917f5d965410 100644 --- a/api_docs/kbn_core_preboot_server_mocks.mdx +++ b/api_docs/kbn_core_preboot_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server-mocks title: "@kbn/core-preboot-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server-mocks'] --- import kbnCorePrebootServerMocksObj from './kbn_core_preboot_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_browser_mocks.mdx b/api_docs/kbn_core_rendering_browser_mocks.mdx index bf6439509558a..e23865cba3d2c 100644 --- a/api_docs/kbn_core_rendering_browser_mocks.mdx +++ b/api_docs/kbn_core_rendering_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-browser-mocks title: "@kbn/core-rendering-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-browser-mocks'] --- import kbnCoreRenderingBrowserMocksObj from './kbn_core_rendering_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_internal.mdx b/api_docs/kbn_core_rendering_server_internal.mdx index ddfe22e54e4b1..8022c767b0fd5 100644 --- a/api_docs/kbn_core_rendering_server_internal.mdx +++ b/api_docs/kbn_core_rendering_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-internal title: "@kbn/core-rendering-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-internal'] --- import kbnCoreRenderingServerInternalObj from './kbn_core_rendering_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_mocks.mdx b/api_docs/kbn_core_rendering_server_mocks.mdx index da68abb3cd0a5..a0326d288522b 100644 --- a/api_docs/kbn_core_rendering_server_mocks.mdx +++ b/api_docs/kbn_core_rendering_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-mocks title: "@kbn/core-rendering-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-mocks'] --- import kbnCoreRenderingServerMocksObj from './kbn_core_rendering_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_root_server_internal.mdx b/api_docs/kbn_core_root_server_internal.mdx index 13310806c251b..e954955055b28 100644 --- a/api_docs/kbn_core_root_server_internal.mdx +++ b/api_docs/kbn_core_root_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-root-server-internal title: "@kbn/core-root-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-root-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-root-server-internal'] --- import kbnCoreRootServerInternalObj from './kbn_core_root_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_browser.mdx b/api_docs/kbn_core_saved_objects_api_browser.mdx index 445b76871e070..d098d6fc6a877 100644 --- a/api_docs/kbn_core_saved_objects_api_browser.mdx +++ b/api_docs/kbn_core_saved_objects_api_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-browser title: "@kbn/core-saved-objects-api-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-browser'] --- import kbnCoreSavedObjectsApiBrowserObj from './kbn_core_saved_objects_api_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server.mdx b/api_docs/kbn_core_saved_objects_api_server.mdx index 3472fb1d0e556..fc90916390924 100644 --- a/api_docs/kbn_core_saved_objects_api_server.mdx +++ b/api_docs/kbn_core_saved_objects_api_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server title: "@kbn/core-saved-objects-api-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server'] --- import kbnCoreSavedObjectsApiServerObj from './kbn_core_saved_objects_api_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_internal.mdx b/api_docs/kbn_core_saved_objects_api_server_internal.mdx index 573ada980ab13..c1bf32bdc81e3 100644 --- a/api_docs/kbn_core_saved_objects_api_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-internal title: "@kbn/core-saved-objects-api-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-internal'] --- import kbnCoreSavedObjectsApiServerInternalObj from './kbn_core_saved_objects_api_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx index 84ba363fc4abf..4a719a37c1927 100644 --- a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-mocks title: "@kbn/core-saved-objects-api-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-mocks'] --- import kbnCoreSavedObjectsApiServerMocksObj from './kbn_core_saved_objects_api_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_internal.mdx b/api_docs/kbn_core_saved_objects_base_server_internal.mdx index e92a126af1bc7..da1cc36d5396a 100644 --- a/api_docs/kbn_core_saved_objects_base_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-internal title: "@kbn/core-saved-objects-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-internal'] --- import kbnCoreSavedObjectsBaseServerInternalObj from './kbn_core_saved_objects_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx index 77048023b7e0c..21eee5d623eab 100644 --- a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-mocks title: "@kbn/core-saved-objects-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-mocks'] --- import kbnCoreSavedObjectsBaseServerMocksObj from './kbn_core_saved_objects_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser.mdx b/api_docs/kbn_core_saved_objects_browser.mdx index 49fb2311c59b6..196d2c0542412 100644 --- a/api_docs/kbn_core_saved_objects_browser.mdx +++ b/api_docs/kbn_core_saved_objects_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser title: "@kbn/core-saved-objects-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser'] --- import kbnCoreSavedObjectsBrowserObj from './kbn_core_saved_objects_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_internal.mdx b/api_docs/kbn_core_saved_objects_browser_internal.mdx index be9b42ac53903..fb0954f8ba6c7 100644 --- a/api_docs/kbn_core_saved_objects_browser_internal.mdx +++ b/api_docs/kbn_core_saved_objects_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-internal title: "@kbn/core-saved-objects-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-internal'] --- import kbnCoreSavedObjectsBrowserInternalObj from './kbn_core_saved_objects_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_mocks.mdx b/api_docs/kbn_core_saved_objects_browser_mocks.mdx index f448abdf3e2cb..56d4834bd178f 100644 --- a/api_docs/kbn_core_saved_objects_browser_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-mocks title: "@kbn/core-saved-objects-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-mocks'] --- import kbnCoreSavedObjectsBrowserMocksObj from './kbn_core_saved_objects_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_common.mdx b/api_docs/kbn_core_saved_objects_common.mdx index c35dda2a42e3c..9f20670489b8d 100644 --- a/api_docs/kbn_core_saved_objects_common.mdx +++ b/api_docs/kbn_core_saved_objects_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-common title: "@kbn/core-saved-objects-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-common plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-common'] --- import kbnCoreSavedObjectsCommonObj from './kbn_core_saved_objects_common.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx index 9e5c9709fdd39..3737c024b1811 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-internal title: "@kbn/core-saved-objects-import-export-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-internal'] --- import kbnCoreSavedObjectsImportExportServerInternalObj from './kbn_core_saved_objects_import_export_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx index 7572a2334921c..1673f46406aee 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-mocks title: "@kbn/core-saved-objects-import-export-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-mocks'] --- import kbnCoreSavedObjectsImportExportServerMocksObj from './kbn_core_saved_objects_import_export_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx index c9b6050fb01c4..55f26157ee855 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-internal title: "@kbn/core-saved-objects-migration-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-internal'] --- import kbnCoreSavedObjectsMigrationServerInternalObj from './kbn_core_saved_objects_migration_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx index 7d8ed4ab6ab1a..dd52cc2b3da7d 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-mocks title: "@kbn/core-saved-objects-migration-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-mocks'] --- import kbnCoreSavedObjectsMigrationServerMocksObj from './kbn_core_saved_objects_migration_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server.devdocs.json b/api_docs/kbn_core_saved_objects_server.devdocs.json index 40177ae597173..78be56369b3f8 100644 --- a/api_docs/kbn_core_saved_objects_server.devdocs.json +++ b/api_docs/kbn_core_saved_objects_server.devdocs.json @@ -968,12 +968,12 @@ "children": [ { "parentPluginId": "@kbn/core-saved-objects-server", - "id": "def-server.ISavedObjectsSecurityExtension.checkAuthorization", + "id": "def-server.ISavedObjectsSecurityExtension.performAuthorization", "type": "Function", "tags": [], - "label": "checkAuthorization", + "label": "performAuthorization", "description": [ - "\nChecks authorization of actions on specified types in specified spaces." + "\nPerforms authorization (check & enforce) of actions on specified types in specified spaces." ], "signature": [ "(params: ", @@ -981,8 +981,8 @@ "pluginId": "@kbn/core-saved-objects-server", "scope": "server", "docId": "kibKbnCoreSavedObjectsServerPluginApi", - "section": "def-server.CheckAuthorizationParams", - "text": "CheckAuthorizationParams" + "section": "def-server.PerformAuthorizationParams", + "text": "PerformAuthorizationParams" }, ") => Promise<", { @@ -1000,20 +1000,20 @@ "children": [ { "parentPluginId": "@kbn/core-saved-objects-server", - "id": "def-server.ISavedObjectsSecurityExtension.checkAuthorization.$1", + "id": "def-server.ISavedObjectsSecurityExtension.performAuthorization.$1", "type": "Object", "tags": [], "label": "params", "description": [ - "- types, spaces, and actions to check" + "- actions, types & spaces map, audit callback, options (enforce bypassed if enforce map is undefined)" ], "signature": [ { "pluginId": "@kbn/core-saved-objects-server", "scope": "server", "docId": "kibKbnCoreSavedObjectsServerPluginApi", - "section": "def-server.CheckAuthorizationParams", - "text": "CheckAuthorizationParams" + "section": "def-server.PerformAuthorizationParams", + "text": "PerformAuthorizationParams" }, "" ], @@ -2036,6 +2036,146 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/core-saved-objects-server", + "id": "def-server.PerformAuthorizationParams", + "type": "Interface", + "tags": [], + "label": "PerformAuthorizationParams", + "description": [ + "\nThe PerformAuthorizationParams interface contains settings for checking\n& enforcing authorization via the ISavedObjectsSecurityExtension." + ], + "signature": [ + { + "pluginId": "@kbn/core-saved-objects-server", + "scope": "server", + "docId": "kibKbnCoreSavedObjectsServerPluginApi", + "section": "def-server.PerformAuthorizationParams", + "text": "PerformAuthorizationParams" + }, + "" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-saved-objects-server", + "id": "def-server.PerformAuthorizationParams.actions", + "type": "Object", + "tags": [], + "label": "actions", + "description": [ + "\nA set of actions to check." + ], + "signature": [ + "Set" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-saved-objects-server", + "id": "def-server.PerformAuthorizationParams.types", + "type": "Object", + "tags": [], + "label": "types", + "description": [ + "\nA set of types to check." + ], + "signature": [ + "Set" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-saved-objects-server", + "id": "def-server.PerformAuthorizationParams.spaces", + "type": "Object", + "tags": [], + "label": "spaces", + "description": [ + "\nA set of spaces to check (types to check comes from the typesAndSpaces map)." + ], + "signature": [ + "Set" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-saved-objects-server", + "id": "def-server.PerformAuthorizationParams.enforceMap", + "type": "Object", + "tags": [], + "label": "enforceMap", + "description": [ + "\nA map of types (key) to spaces (value) that will be affected by the action(s).\nIf undefined, enforce with be bypassed." + ], + "signature": [ + "Map> | undefined" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/core-saved-objects-server", + "id": "def-server.PerformAuthorizationParams.auditCallback", + "type": "Function", + "tags": [], + "label": "auditCallback", + "description": [ + "\nA callback intended to handle adding audit events in\nboth error (unauthorized), or success (authorized)\ncases" + ], + "signature": [ + "((error?: Error | undefined) => void) | undefined" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-saved-objects-server", + "id": "def-server.PerformAuthorizationParams.auditCallback.$1", + "type": "Object", + "tags": [], + "label": "error", + "description": [], + "signature": [ + "Error | undefined" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-saved-objects-server", + "id": "def-server.PerformAuthorizationParams.options", + "type": "Object", + "tags": [], + "label": "options", + "description": [ + "\nAuthorization options\nallowGlobalResource - whether or not to allow global resources, false if options are undefined" + ], + "signature": [ + "{ allowGlobalResource: boolean; } | undefined" + ], + "path": "packages/core/saved-objects/core-saved-objects-server/src/extensions/security.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/core-saved-objects-server", "id": "def-server.RedactNamespacesParams", diff --git a/api_docs/kbn_core_saved_objects_server.mdx b/api_docs/kbn_core_saved_objects_server.mdx index 340ec57719a8a..a279ed390446e 100644 --- a/api_docs/kbn_core_saved_objects_server.mdx +++ b/api_docs/kbn_core_saved_objects_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server title: "@kbn/core-saved-objects-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server'] --- import kbnCoreSavedObjectsServerObj from './kbn_core_saved_objects_server.devdocs.json'; @@ -21,7 +21,7 @@ Contact Kibana Core for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 297 | 0 | 90 | 0 | +| 305 | 0 | 91 | 0 | ## Server diff --git a/api_docs/kbn_core_saved_objects_server_internal.mdx b/api_docs/kbn_core_saved_objects_server_internal.mdx index e9122731f70dd..0a770e71ea20d 100644 --- a/api_docs/kbn_core_saved_objects_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-internal title: "@kbn/core-saved-objects-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-internal'] --- import kbnCoreSavedObjectsServerInternalObj from './kbn_core_saved_objects_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_mocks.mdx b/api_docs/kbn_core_saved_objects_server_mocks.mdx index c250d95d9bb92..7ce37656a5653 100644 --- a/api_docs/kbn_core_saved_objects_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-mocks title: "@kbn/core-saved-objects-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-mocks'] --- import kbnCoreSavedObjectsServerMocksObj from './kbn_core_saved_objects_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_utils_server.devdocs.json b/api_docs/kbn_core_saved_objects_utils_server.devdocs.json index 82dc05f2d9e72..b0d192a1217ce 100644 --- a/api_docs/kbn_core_saved_objects_utils_server.devdocs.json +++ b/api_docs/kbn_core_saved_objects_utils_server.devdocs.json @@ -1779,6 +1779,62 @@ } ], "functions": [ + { + "parentPluginId": "@kbn/core-saved-objects-utils-server", + "id": "def-server.arrayMapsAreEqual", + "type": "Function", + "tags": [], + "label": "arrayMapsAreEqual", + "description": [ + "\nDetermines if a given map of arrays is equal to another given map of arrays.\nUsed for comparing namespace maps in saved object repo/security extension tests.\n" + ], + "signature": [ + "(mapA: Map, mapB: Map) => boolean" + ], + "path": "packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-saved-objects-utils-server", + "id": "def-server.arrayMapsAreEqual.$1", + "type": "Object", + "tags": [], + "label": "mapA", + "description": [ + "The first map to compare" + ], + "signature": [ + "Map" + ], + "path": "packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/core-saved-objects-utils-server", + "id": "def-server.arrayMapsAreEqual.$2", + "type": "Object", + "tags": [], + "label": "mapB", + "description": [ + "The second map to compare" + ], + "signature": [ + "Map" + ], + "path": "packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [ + "True if map A is equal to map B" + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/core-saved-objects-utils-server", "id": "def-server.mergeSavedObjectMigrationMaps", @@ -1869,6 +1925,118 @@ "The merged map {@link SavedObjectMigrationMap }" ], "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-saved-objects-utils-server", + "id": "def-server.setMapsAreEqual", + "type": "Function", + "tags": [], + "label": "setMapsAreEqual", + "description": [ + "\nDetermines if a given Map of Sets is equal to another given Map of Sets.\nUsed for comparing typeMaps and enforceMaps in saved object repo/security extension tests.\n" + ], + "signature": [ + "(mapA: Map> | undefined, mapB: Map> | undefined) => boolean" + ], + "path": "packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-saved-objects-utils-server", + "id": "def-server.setMapsAreEqual.$1", + "type": "Object", + "tags": [], + "label": "mapA", + "description": [ + "The first map to compare" + ], + "signature": [ + "Map> | undefined" + ], + "path": "packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + }, + { + "parentPluginId": "@kbn/core-saved-objects-utils-server", + "id": "def-server.setMapsAreEqual.$2", + "type": "Object", + "tags": [], + "label": "mapB", + "description": [ + "The second map to compare" + ], + "signature": [ + "Map> | undefined" + ], + "path": "packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [ + "True if map A is equal to map B" + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-saved-objects-utils-server", + "id": "def-server.setsAreEqual", + "type": "Function", + "tags": [], + "label": "setsAreEqual", + "description": [ + "\nDetermines if a given Set is equal to another given Set. Set types must be the same, and comparable.\n" + ], + "signature": [ + "(setA: Set, setB: Set) => boolean" + ], + "path": "packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/core-saved-objects-utils-server", + "id": "def-server.setsAreEqual.$1", + "type": "Object", + "tags": [], + "label": "setA", + "description": [ + "The first Set to compare" + ], + "signature": [ + "Set" + ], + "path": "packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/core-saved-objects-utils-server", + "id": "def-server.setsAreEqual.$2", + "type": "Object", + "tags": [], + "label": "setB", + "description": [ + "The second Set to compare" + ], + "signature": [ + "Set" + ], + "path": "packages/core/saved-objects/core-saved-objects-utils-server/src/saved_objects_test_utils.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [ + "True if Set A is equal to Set B" + ], + "initialIsOpen": false } ], "interfaces": [ diff --git a/api_docs/kbn_core_saved_objects_utils_server.mdx b/api_docs/kbn_core_saved_objects_utils_server.mdx index a43dfa3595939..fbe0230302c86 100644 --- a/api_docs/kbn_core_saved_objects_utils_server.mdx +++ b/api_docs/kbn_core_saved_objects_utils_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-utils-server title: "@kbn/core-saved-objects-utils-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-utils-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-utils-server'] --- import kbnCoreSavedObjectsUtilsServerObj from './kbn_core_saved_objects_utils_server.devdocs.json'; @@ -21,7 +21,7 @@ Contact Kibana Core for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 99 | 1 | 84 | 0 | +| 108 | 1 | 84 | 0 | ## Server diff --git a/api_docs/kbn_core_status_common.mdx b/api_docs/kbn_core_status_common.mdx index 9fd68a492605d..e229fb10b3c1b 100644 --- a/api_docs/kbn_core_status_common.mdx +++ b/api_docs/kbn_core_status_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common title: "@kbn/core-status-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common'] --- import kbnCoreStatusCommonObj from './kbn_core_status_common.devdocs.json'; diff --git a/api_docs/kbn_core_status_common_internal.mdx b/api_docs/kbn_core_status_common_internal.mdx index 7ec5660168700..42008f701c29e 100644 --- a/api_docs/kbn_core_status_common_internal.mdx +++ b/api_docs/kbn_core_status_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common-internal title: "@kbn/core-status-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common-internal'] --- import kbnCoreStatusCommonInternalObj from './kbn_core_status_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server.mdx b/api_docs/kbn_core_status_server.mdx index 1afb674c91312..d8b1c49a5d310 100644 --- a/api_docs/kbn_core_status_server.mdx +++ b/api_docs/kbn_core_status_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server title: "@kbn/core-status-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server'] --- import kbnCoreStatusServerObj from './kbn_core_status_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_internal.mdx b/api_docs/kbn_core_status_server_internal.mdx index c3920856026f5..fd13d6b624a18 100644 --- a/api_docs/kbn_core_status_server_internal.mdx +++ b/api_docs/kbn_core_status_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-internal title: "@kbn/core-status-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-internal'] --- import kbnCoreStatusServerInternalObj from './kbn_core_status_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_mocks.mdx b/api_docs/kbn_core_status_server_mocks.mdx index da4f0474c2206..ba4a53f5cbf86 100644 --- a/api_docs/kbn_core_status_server_mocks.mdx +++ b/api_docs/kbn_core_status_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-mocks title: "@kbn/core-status-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-mocks'] --- import kbnCoreStatusServerMocksObj from './kbn_core_status_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx index 7d5089cf7518f..db297356a379a 100644 --- a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx +++ b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-deprecations-getters title: "@kbn/core-test-helpers-deprecations-getters" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-deprecations-getters plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-deprecations-getters'] --- import kbnCoreTestHelpersDeprecationsGettersObj from './kbn_core_test_helpers_deprecations_getters.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx index 041eb1c184ed1..1694ad503e7d7 100644 --- a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx +++ b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-http-setup-browser title: "@kbn/core-test-helpers-http-setup-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-http-setup-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-http-setup-browser'] --- import kbnCoreTestHelpersHttpSetupBrowserObj from './kbn_core_test_helpers_http_setup_browser.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_kbn_server.mdx b/api_docs/kbn_core_test_helpers_kbn_server.mdx index 1dd2f1f348a5c..1c6c3b40bdb2a 100644 --- a/api_docs/kbn_core_test_helpers_kbn_server.mdx +++ b/api_docs/kbn_core_test_helpers_kbn_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-kbn-server title: "@kbn/core-test-helpers-kbn-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-kbn-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-kbn-server'] --- import kbnCoreTestHelpersKbnServerObj from './kbn_core_test_helpers_kbn_server.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx index 22e61b2935dfd..7d84855ffb15b 100644 --- a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx +++ b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-so-type-serializer title: "@kbn/core-test-helpers-so-type-serializer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-so-type-serializer plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-so-type-serializer'] --- import kbnCoreTestHelpersSoTypeSerializerObj from './kbn_core_test_helpers_so_type_serializer.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_test_utils.mdx b/api_docs/kbn_core_test_helpers_test_utils.mdx index 8852b0bc4398d..a2526baacc0dc 100644 --- a/api_docs/kbn_core_test_helpers_test_utils.mdx +++ b/api_docs/kbn_core_test_helpers_test_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-test-utils title: "@kbn/core-test-helpers-test-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-test-utils plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-test-utils'] --- import kbnCoreTestHelpersTestUtilsObj from './kbn_core_test_helpers_test_utils.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser.mdx b/api_docs/kbn_core_theme_browser.mdx index 018bab1a11719..0c0b725938ff8 100644 --- a/api_docs/kbn_core_theme_browser.mdx +++ b/api_docs/kbn_core_theme_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser title: "@kbn/core-theme-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser'] --- import kbnCoreThemeBrowserObj from './kbn_core_theme_browser.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_internal.mdx b/api_docs/kbn_core_theme_browser_internal.mdx index 7f47cf85060ee..3c33004a268f2 100644 --- a/api_docs/kbn_core_theme_browser_internal.mdx +++ b/api_docs/kbn_core_theme_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-internal title: "@kbn/core-theme-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-internal'] --- import kbnCoreThemeBrowserInternalObj from './kbn_core_theme_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_mocks.mdx b/api_docs/kbn_core_theme_browser_mocks.mdx index fda918fae7cb0..a221e2abd4bb9 100644 --- a/api_docs/kbn_core_theme_browser_mocks.mdx +++ b/api_docs/kbn_core_theme_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-mocks title: "@kbn/core-theme-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-mocks'] --- import kbnCoreThemeBrowserMocksObj from './kbn_core_theme_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser.mdx b/api_docs/kbn_core_ui_settings_browser.mdx index 95909bf0f4ece..6fd76ed804270 100644 --- a/api_docs/kbn_core_ui_settings_browser.mdx +++ b/api_docs/kbn_core_ui_settings_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser title: "@kbn/core-ui-settings-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser'] --- import kbnCoreUiSettingsBrowserObj from './kbn_core_ui_settings_browser.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_internal.mdx b/api_docs/kbn_core_ui_settings_browser_internal.mdx index c604349c6b7ea..90405389d7712 100644 --- a/api_docs/kbn_core_ui_settings_browser_internal.mdx +++ b/api_docs/kbn_core_ui_settings_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-internal title: "@kbn/core-ui-settings-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-internal'] --- import kbnCoreUiSettingsBrowserInternalObj from './kbn_core_ui_settings_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_mocks.mdx b/api_docs/kbn_core_ui_settings_browser_mocks.mdx index 2687e7287c052..f5fdb735eab30 100644 --- a/api_docs/kbn_core_ui_settings_browser_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-mocks title: "@kbn/core-ui-settings-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-mocks'] --- import kbnCoreUiSettingsBrowserMocksObj from './kbn_core_ui_settings_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_common.mdx b/api_docs/kbn_core_ui_settings_common.mdx index 68da8cc9a72ad..176c1b6b3d6f9 100644 --- a/api_docs/kbn_core_ui_settings_common.mdx +++ b/api_docs/kbn_core_ui_settings_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-common title: "@kbn/core-ui-settings-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-common plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-common'] --- import kbnCoreUiSettingsCommonObj from './kbn_core_ui_settings_common.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server.mdx b/api_docs/kbn_core_ui_settings_server.mdx index 53b64c38f5223..abfd1c6a2063c 100644 --- a/api_docs/kbn_core_ui_settings_server.mdx +++ b/api_docs/kbn_core_ui_settings_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server title: "@kbn/core-ui-settings-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server'] --- import kbnCoreUiSettingsServerObj from './kbn_core_ui_settings_server.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_internal.mdx b/api_docs/kbn_core_ui_settings_server_internal.mdx index cb7600fa57aeb..2de8c8d984efa 100644 --- a/api_docs/kbn_core_ui_settings_server_internal.mdx +++ b/api_docs/kbn_core_ui_settings_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-internal title: "@kbn/core-ui-settings-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-internal'] --- import kbnCoreUiSettingsServerInternalObj from './kbn_core_ui_settings_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_mocks.mdx b/api_docs/kbn_core_ui_settings_server_mocks.mdx index 7dd1fe9b2ef80..4a3b71dc4fb24 100644 --- a/api_docs/kbn_core_ui_settings_server_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-mocks title: "@kbn/core-ui-settings-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-mocks'] --- import kbnCoreUiSettingsServerMocksObj from './kbn_core_ui_settings_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server.mdx b/api_docs/kbn_core_usage_data_server.mdx index f895d15c8f0a8..f1e4393db34b3 100644 --- a/api_docs/kbn_core_usage_data_server.mdx +++ b/api_docs/kbn_core_usage_data_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server title: "@kbn/core-usage-data-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server'] --- import kbnCoreUsageDataServerObj from './kbn_core_usage_data_server.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_internal.mdx b/api_docs/kbn_core_usage_data_server_internal.mdx index a14c69c09aa8d..9a1768a959fc8 100644 --- a/api_docs/kbn_core_usage_data_server_internal.mdx +++ b/api_docs/kbn_core_usage_data_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-internal title: "@kbn/core-usage-data-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-internal plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-internal'] --- import kbnCoreUsageDataServerInternalObj from './kbn_core_usage_data_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_mocks.mdx b/api_docs/kbn_core_usage_data_server_mocks.mdx index 0f21fd3a41dbb..d8a44386e6903 100644 --- a/api_docs/kbn_core_usage_data_server_mocks.mdx +++ b/api_docs/kbn_core_usage_data_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-mocks title: "@kbn/core-usage-data-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-mocks'] --- import kbnCoreUsageDataServerMocksObj from './kbn_core_usage_data_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_crypto.mdx b/api_docs/kbn_crypto.mdx index 0ec5a65affeb4..c8f7400822c68 100644 --- a/api_docs/kbn_crypto.mdx +++ b/api_docs/kbn_crypto.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto title: "@kbn/crypto" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto'] --- import kbnCryptoObj from './kbn_crypto.devdocs.json'; diff --git a/api_docs/kbn_crypto_browser.mdx b/api_docs/kbn_crypto_browser.mdx index 35660b45c08da..91489ac079ec9 100644 --- a/api_docs/kbn_crypto_browser.mdx +++ b/api_docs/kbn_crypto_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto-browser title: "@kbn/crypto-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto-browser plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto-browser'] --- import kbnCryptoBrowserObj from './kbn_crypto_browser.devdocs.json'; diff --git a/api_docs/kbn_datemath.mdx b/api_docs/kbn_datemath.mdx index 84262c02b8309..ae1d07fc6987d 100644 --- a/api_docs/kbn_datemath.mdx +++ b/api_docs/kbn_datemath.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-datemath title: "@kbn/datemath" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/datemath plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/datemath'] --- import kbnDatemathObj from './kbn_datemath.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_errors.mdx b/api_docs/kbn_dev_cli_errors.mdx index cc51a043d63e2..b97050dbe49d9 100644 --- a/api_docs/kbn_dev_cli_errors.mdx +++ b/api_docs/kbn_dev_cli_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-errors title: "@kbn/dev-cli-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-errors plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-errors'] --- import kbnDevCliErrorsObj from './kbn_dev_cli_errors.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_runner.mdx b/api_docs/kbn_dev_cli_runner.mdx index 1cb6d4ae3d002..6a3f3d228ebe6 100644 --- a/api_docs/kbn_dev_cli_runner.mdx +++ b/api_docs/kbn_dev_cli_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-runner title: "@kbn/dev-cli-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-runner plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-runner'] --- import kbnDevCliRunnerObj from './kbn_dev_cli_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_proc_runner.mdx b/api_docs/kbn_dev_proc_runner.mdx index 13c93864d004d..a1eb9a16fcb79 100644 --- a/api_docs/kbn_dev_proc_runner.mdx +++ b/api_docs/kbn_dev_proc_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-proc-runner title: "@kbn/dev-proc-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-proc-runner plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-proc-runner'] --- import kbnDevProcRunnerObj from './kbn_dev_proc_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_utils.mdx b/api_docs/kbn_dev_utils.mdx index 35672c983875e..62848a885d886 100644 --- a/api_docs/kbn_dev_utils.mdx +++ b/api_docs/kbn_dev_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-utils title: "@kbn/dev-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-utils plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-utils'] --- import kbnDevUtilsObj from './kbn_dev_utils.devdocs.json'; diff --git a/api_docs/kbn_doc_links.mdx b/api_docs/kbn_doc_links.mdx index 974d2ba08474d..1a2b227049cc8 100644 --- a/api_docs/kbn_doc_links.mdx +++ b/api_docs/kbn_doc_links.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-doc-links title: "@kbn/doc-links" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/doc-links plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/doc-links'] --- import kbnDocLinksObj from './kbn_doc_links.devdocs.json'; diff --git a/api_docs/kbn_docs_utils.mdx b/api_docs/kbn_docs_utils.mdx index 6980ed1b6016a..786e7da34f931 100644 --- a/api_docs/kbn_docs_utils.mdx +++ b/api_docs/kbn_docs_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-docs-utils title: "@kbn/docs-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/docs-utils plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/docs-utils'] --- import kbnDocsUtilsObj from './kbn_docs_utils.devdocs.json'; diff --git a/api_docs/kbn_ebt_tools.mdx b/api_docs/kbn_ebt_tools.mdx index c15da8a640e64..04d70ce438b68 100644 --- a/api_docs/kbn_ebt_tools.mdx +++ b/api_docs/kbn_ebt_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ebt-tools title: "@kbn/ebt-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ebt-tools plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ebt-tools'] --- import kbnEbtToolsObj from './kbn_ebt_tools.devdocs.json'; diff --git a/api_docs/kbn_ecs.mdx b/api_docs/kbn_ecs.mdx index 4086cb00f8adf..6631160525ceb 100644 --- a/api_docs/kbn_ecs.mdx +++ b/api_docs/kbn_ecs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ecs title: "@kbn/ecs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ecs plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ecs'] --- import kbnEcsObj from './kbn_ecs.devdocs.json'; diff --git a/api_docs/kbn_es.mdx b/api_docs/kbn_es.mdx index 915d748ac9bd5..2c87affca4ce7 100644 --- a/api_docs/kbn_es.mdx +++ b/api_docs/kbn_es.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es title: "@kbn/es" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es'] --- import kbnEsObj from './kbn_es.devdocs.json'; diff --git a/api_docs/kbn_es_archiver.mdx b/api_docs/kbn_es_archiver.mdx index 519d4dbfd4587..688e1e3dcd945 100644 --- a/api_docs/kbn_es_archiver.mdx +++ b/api_docs/kbn_es_archiver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-archiver title: "@kbn/es-archiver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-archiver plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-archiver'] --- import kbnEsArchiverObj from './kbn_es_archiver.devdocs.json'; diff --git a/api_docs/kbn_es_errors.mdx b/api_docs/kbn_es_errors.mdx index 66d841b40ac1c..06c378f966cdc 100644 --- a/api_docs/kbn_es_errors.mdx +++ b/api_docs/kbn_es_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-errors title: "@kbn/es-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-errors plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-errors'] --- import kbnEsErrorsObj from './kbn_es_errors.devdocs.json'; diff --git a/api_docs/kbn_es_query.devdocs.json b/api_docs/kbn_es_query.devdocs.json index 0b894c5ecbcba..c29ba715ef6e7 100644 --- a/api_docs/kbn_es_query.devdocs.json +++ b/api_docs/kbn_es_query.devdocs.json @@ -111,19 +111,49 @@ "tags": [], "label": "buildCombinedFilter", "description": [ - "\nBuilds an COMBINED filter. An COMBINED filter is a filter with multiple sub-filters. Each sub-filter (FilterItem) represents a\ncondition." + "\nBuilds an COMBINED filter. An COMBINED filter is a filter with multiple sub-filters. Each sub-filter (FilterItem)\nrepresents a condition." ], "signature": [ - "(filters: ", + "(relation: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.BooleanRelation", + "text": "BooleanRelation" + }, + ", filters: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + "[], indexPattern: ", { "pluginId": "@kbn/es-query", "scope": "common", "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.FilterItem", - "text": "FilterItem" + "section": "def-common.DataViewBase", + "text": "DataViewBase" + }, + ", disabled: boolean | undefined, negate: boolean | undefined, alias: string | null | undefined, store: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.FilterStateStore", + "text": "FilterStateStore" }, - "[]) => ", - "CombinedFilter" + ") => ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.CombinedFilter", + "text": "CombinedFilter" + } ], "path": "packages/kbn-es-query/src/filters/build_filters/combined_filter.ts", "deprecated": false, @@ -132,19 +162,42 @@ { "parentPluginId": "@kbn/es-query", "id": "def-common.buildCombinedFilter.$1", + "type": "Enum", + "tags": [], + "label": "relation", + "description": [ + "The type of relation with which to combine the filters (AND/OR)" + ], + "signature": [ + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.BooleanRelation", + "text": "BooleanRelation" + } + ], + "path": "packages/kbn-es-query/src/filters/build_filters/combined_filter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.buildCombinedFilter.$2", "type": "Array", "tags": [], "label": "filters", "description": [ - "An array of CombinedFilterItem" + "An array of sub-filters" ], "signature": [ { "pluginId": "@kbn/es-query", "scope": "common", "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.FilterItem", - "text": "FilterItem" + "section": "def-common.Filter", + "text": "Filter" }, "[]" ], @@ -152,6 +205,93 @@ "deprecated": false, "trackAdoption": false, "isRequired": true + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.buildCombinedFilter.$3", + "type": "Object", + "tags": [], + "label": "indexPattern", + "description": [], + "signature": [ + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewBase", + "text": "DataViewBase" + } + ], + "path": "packages/kbn-es-query/src/filters/build_filters/combined_filter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.buildCombinedFilter.$4", + "type": "CompoundType", + "tags": [], + "label": "disabled", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-es-query/src/filters/build_filters/combined_filter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.buildCombinedFilter.$5", + "type": "CompoundType", + "tags": [], + "label": "negate", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-es-query/src/filters/build_filters/combined_filter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.buildCombinedFilter.$6", + "type": "CompoundType", + "tags": [], + "label": "alias", + "description": [], + "signature": [ + "string | null | undefined" + ], + "path": "packages/kbn-es-query/src/filters/build_filters/combined_filter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.buildCombinedFilter.$7", + "type": "Enum", + "tags": [], + "label": "store", + "description": [], + "signature": [ + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.FilterStateStore", + "text": "FilterStateStore" + } + ], + "path": "packages/kbn-es-query/src/filters/build_filters/combined_filter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true } ], "returnComment": [], @@ -2061,6 +2201,135 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.fromCombinedFilter", + "type": "Function", + "tags": [], + "label": "fromCombinedFilter", + "description": [], + "signature": [ + "(filter: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + ", dataViews?: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewBase", + "text": "DataViewBase" + }, + " | ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewBase", + "text": "DataViewBase" + }, + "[] | undefined, options?: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.EsQueryFiltersConfig", + "text": "EsQueryFiltersConfig" + }, + ") => ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + } + ], + "path": "packages/kbn-es-query/src/es_query/from_combined_filter.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.fromCombinedFilter.$1", + "type": "Object", + "tags": [], + "label": "filter", + "description": [], + "signature": [ + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + } + ], + "path": "packages/kbn-es-query/src/es_query/from_combined_filter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.fromCombinedFilter.$2", + "type": "CompoundType", + "tags": [], + "label": "dataViews", + "description": [], + "signature": [ + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewBase", + "text": "DataViewBase" + }, + " | ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewBase", + "text": "DataViewBase" + }, + "[] | undefined" + ], + "path": "packages/kbn-es-query/src/es_query/from_combined_filter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.fromCombinedFilter.$3", + "type": "Object", + "tags": [], + "label": "options", + "description": [], + "signature": [ + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.EsQueryFiltersConfig", + "text": "EsQueryFiltersConfig" + } + ], + "path": "packages/kbn-es-query/src/es_query/from_combined_filter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/es-query", "id": "def-common.fromKueryExpression", @@ -3761,6 +4030,138 @@ "An unpinned (app scoped) copy of the filter" ], "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.updateFilter", + "type": "Function", + "tags": [], + "label": "updateFilter", + "description": [], + "signature": [ + "(filter: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + ", field?: string | undefined, operator?: FilterOperator | undefined, params?: any) => { meta: { key: string | undefined; field: string | undefined; params: { query: undefined; }; value: undefined; type: undefined; alias?: string | null | undefined; disabled?: boolean | undefined; negate?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; }; query: undefined; $state?: { store: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.FilterStateStore", + "text": "FilterStateStore" + }, + "; } | undefined; } | { meta: { negate: boolean | undefined; type: string | undefined; params: undefined; value: string; alias?: string | null | undefined; disabled?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; key?: string | undefined; }; query: { exists: { field: string | undefined; }; }; $state?: { store: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.FilterStateStore", + "text": "FilterStateStore" + }, + "; } | undefined; } | { meta: { negate: boolean | undefined; type: string | undefined; params: any; alias?: string | null | undefined; disabled?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; key?: string | undefined; value?: string | undefined; }; query: { range: { [x: string]: any; }; }; $state?: { store: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.FilterStateStore", + "text": "FilterStateStore" + }, + "; } | undefined; } | { meta: { negate: boolean | undefined; type: string | undefined; params: any[] | undefined; alias?: string | null | undefined; disabled?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; key?: string | undefined; value?: string | undefined; }; query: { bool: any; }; $state?: { store: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.FilterStateStore", + "text": "FilterStateStore" + }, + "; } | undefined; } | { meta: { negate: boolean | undefined; type: string | undefined; params: any; alias?: string | null | undefined; disabled?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; key?: string | undefined; value?: string | undefined; }; query: { match_phrase: { [x: string]: any; }; }; $state?: { store: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.FilterStateStore", + "text": "FilterStateStore" + }, + "; } | undefined; }" + ], + "path": "packages/kbn-es-query/src/filters/helpers/update_filter.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.updateFilter.$1", + "type": "Object", + "tags": [], + "label": "filter", + "description": [], + "signature": [ + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + } + ], + "path": "packages/kbn-es-query/src/filters/helpers/update_filter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.updateFilter.$2", + "type": "string", + "tags": [], + "label": "field", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-es-query/src/filters/helpers/update_filter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.updateFilter.$3", + "type": "Object", + "tags": [], + "label": "operator", + "description": [], + "signature": [ + "FilterOperator | undefined" + ], + "path": "packages/kbn-es-query/src/filters/helpers/update_filter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.updateFilter.$4", + "type": "Any", + "tags": [], + "label": "params", + "description": [], + "signature": [ + "any" + ], + "path": "packages/kbn-es-query/src/filters/helpers/update_filter.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false } ], "interfaces": [ @@ -3838,6 +4239,51 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.CombinedFilter", + "type": "Interface", + "tags": [], + "label": "CombinedFilter", + "description": [], + "signature": [ + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.CombinedFilter", + "text": "CombinedFilter" + }, + " extends ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + } + ], + "path": "packages/kbn-es-query/src/filters/build_filters/combined_filter.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.CombinedFilter.meta", + "type": "Object", + "tags": [], + "label": "meta", + "description": [], + "signature": [ + "CombinedFilterMeta" + ], + "path": "packages/kbn-es-query/src/filters/build_filters/combined_filter.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/es-query", "id": "def-common.EsQueryFiltersConfig", @@ -4336,6 +4782,18 @@ } ], "enums": [ + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.BooleanRelation", + "type": "Enum", + "tags": [], + "label": "BooleanRelation", + "description": [], + "path": "packages/kbn-es-query/src/filters/build_filters/combined_filter.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/es-query", "id": "def-common.FILTERS", @@ -4633,38 +5091,6 @@ "trackAdoption": false, "initialIsOpen": false }, - { - "parentPluginId": "@kbn/es-query", - "id": "def-common.FilterItem", - "type": "Type", - "tags": [], - "label": "FilterItem", - "description": [ - "\nEach item in an COMBINED filter may represent either one filter (to be ORed) or an array of filters (ANDed together before\nbecoming part of the OR clause)." - ], - "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Filter", - "text": "Filter" - }, - " | ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.FilterItem", - "text": "FilterItem" - }, - "[]" - ], - "path": "packages/kbn-es-query/src/filters/build_filters/combined_filter.ts", - "deprecated": false, - "trackAdoption": false, - "initialIsOpen": false - }, { "parentPluginId": "@kbn/es-query", "id": "def-common.FilterMeta", diff --git a/api_docs/kbn_es_query.mdx b/api_docs/kbn_es_query.mdx index 4f4ab188bec84..38d9bbfd622bc 100644 --- a/api_docs/kbn_es_query.mdx +++ b/api_docs/kbn_es_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-query title: "@kbn/es-query" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-query plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-query'] --- import kbnEsQueryObj from './kbn_es_query.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 227 | 1 | 170 | 13 | +| 244 | 2 | 187 | 13 | ## Common diff --git a/api_docs/kbn_es_types.mdx b/api_docs/kbn_es_types.mdx index 8187e89551b1d..90fb164e18859 100644 --- a/api_docs/kbn_es_types.mdx +++ b/api_docs/kbn_es_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-types title: "@kbn/es-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-types plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-types'] --- import kbnEsTypesObj from './kbn_es_types.devdocs.json'; diff --git a/api_docs/kbn_eslint_plugin_imports.mdx b/api_docs/kbn_eslint_plugin_imports.mdx index baf3287c90516..ed69a0c615b3c 100644 --- a/api_docs/kbn_eslint_plugin_imports.mdx +++ b/api_docs/kbn_eslint_plugin_imports.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-eslint-plugin-imports title: "@kbn/eslint-plugin-imports" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/eslint-plugin-imports plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/eslint-plugin-imports'] --- import kbnEslintPluginImportsObj from './kbn_eslint_plugin_imports.devdocs.json'; diff --git a/api_docs/kbn_field_types.mdx b/api_docs/kbn_field_types.mdx index 52d8b5e105018..acdf40dee12d0 100644 --- a/api_docs/kbn_field_types.mdx +++ b/api_docs/kbn_field_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-field-types title: "@kbn/field-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/field-types plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/field-types'] --- import kbnFieldTypesObj from './kbn_field_types.devdocs.json'; diff --git a/api_docs/kbn_find_used_node_modules.mdx b/api_docs/kbn_find_used_node_modules.mdx index 541bf851c33ee..048917ea9ae78 100644 --- a/api_docs/kbn_find_used_node_modules.mdx +++ b/api_docs/kbn_find_used_node_modules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-find-used-node-modules title: "@kbn/find-used-node-modules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/find-used-node-modules plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/find-used-node-modules'] --- import kbnFindUsedNodeModulesObj from './kbn_find_used_node_modules.devdocs.json'; diff --git a/api_docs/kbn_ftr_common_functional_services.mdx b/api_docs/kbn_ftr_common_functional_services.mdx index 88eab33246f01..2bb7e88715789 100644 --- a/api_docs/kbn_ftr_common_functional_services.mdx +++ b/api_docs/kbn_ftr_common_functional_services.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ftr-common-functional-services title: "@kbn/ftr-common-functional-services" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ftr-common-functional-services plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ftr-common-functional-services'] --- import kbnFtrCommonFunctionalServicesObj from './kbn_ftr_common_functional_services.devdocs.json'; diff --git a/api_docs/kbn_generate.mdx b/api_docs/kbn_generate.mdx index e9519b4d8a6ec..c8f2120a34d3b 100644 --- a/api_docs/kbn_generate.mdx +++ b/api_docs/kbn_generate.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate title: "@kbn/generate" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate'] --- import kbnGenerateObj from './kbn_generate.devdocs.json'; diff --git a/api_docs/kbn_get_repo_files.mdx b/api_docs/kbn_get_repo_files.mdx index 07cf276e22e72..7eb83e36cafbd 100644 --- a/api_docs/kbn_get_repo_files.mdx +++ b/api_docs/kbn_get_repo_files.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-get-repo-files title: "@kbn/get-repo-files" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/get-repo-files plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/get-repo-files'] --- import kbnGetRepoFilesObj from './kbn_get_repo_files.devdocs.json'; diff --git a/api_docs/kbn_guided_onboarding.mdx b/api_docs/kbn_guided_onboarding.mdx index 7bb6e1ee0212e..4687c940c3d80 100644 --- a/api_docs/kbn_guided_onboarding.mdx +++ b/api_docs/kbn_guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-guided-onboarding title: "@kbn/guided-onboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/guided-onboarding plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/guided-onboarding'] --- import kbnGuidedOnboardingObj from './kbn_guided_onboarding.devdocs.json'; diff --git a/api_docs/kbn_handlebars.devdocs.json b/api_docs/kbn_handlebars.devdocs.json index e1233a052e2cb..b312f840efad0 100644 --- a/api_docs/kbn_handlebars.devdocs.json +++ b/api_docs/kbn_handlebars.devdocs.json @@ -250,7 +250,7 @@ "\nSupported Handlebars compile options.\n\nThis is a subset of all the compile options supported by the upstream\nHandlebars module." ], "signature": [ - "{ data?: boolean | undefined; strict?: boolean | undefined; knownHelpers?: KnownHelpers | undefined; knownHelpersOnly?: boolean | undefined; assumeObjects?: boolean | undefined; noEscape?: boolean | undefined; }" + "{ data?: boolean | undefined; strict?: boolean | undefined; knownHelpers?: KnownHelpers | undefined; knownHelpersOnly?: boolean | undefined; noEscape?: boolean | undefined; assumeObjects?: boolean | undefined; }" ], "path": "packages/kbn-handlebars/index.ts", "deprecated": false, @@ -267,7 +267,7 @@ "\nSupported Handlebars runtime options\n\nThis is a subset of all the runtime options supported by the upstream\nHandlebars module." ], "signature": [ - "{ data?: any; helpers?: { [name: string]: Function; } | undefined; blockParams?: any[] | undefined; decorators?: { [name: string]: Function; } | undefined; }" + "{ data?: any; helpers?: { [name: string]: Function; } | undefined; decorators?: { [name: string]: Function; } | undefined; blockParams?: any[] | undefined; }" ], "path": "packages/kbn-handlebars/index.ts", "deprecated": false, diff --git a/api_docs/kbn_handlebars.mdx b/api_docs/kbn_handlebars.mdx index 60313a4acaa6c..32dec4a918b28 100644 --- a/api_docs/kbn_handlebars.mdx +++ b/api_docs/kbn_handlebars.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-handlebars title: "@kbn/handlebars" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/handlebars plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/handlebars'] --- import kbnHandlebarsObj from './kbn_handlebars.devdocs.json'; diff --git a/api_docs/kbn_hapi_mocks.mdx b/api_docs/kbn_hapi_mocks.mdx index 04ced3f69aaad..6ffdd413df963 100644 --- a/api_docs/kbn_hapi_mocks.mdx +++ b/api_docs/kbn_hapi_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-hapi-mocks title: "@kbn/hapi-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/hapi-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/hapi-mocks'] --- import kbnHapiMocksObj from './kbn_hapi_mocks.devdocs.json'; diff --git a/api_docs/kbn_health_gateway_server.mdx b/api_docs/kbn_health_gateway_server.mdx index 504aaff37622c..a0d6ba50d8759 100644 --- a/api_docs/kbn_health_gateway_server.mdx +++ b/api_docs/kbn_health_gateway_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-health-gateway-server title: "@kbn/health-gateway-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/health-gateway-server plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/health-gateway-server'] --- import kbnHealthGatewayServerObj from './kbn_health_gateway_server.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_card.mdx b/api_docs/kbn_home_sample_data_card.mdx index 4f72351f61c45..45a5d8dae8792 100644 --- a/api_docs/kbn_home_sample_data_card.mdx +++ b/api_docs/kbn_home_sample_data_card.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-card title: "@kbn/home-sample-data-card" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-card plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-card'] --- import kbnHomeSampleDataCardObj from './kbn_home_sample_data_card.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_tab.mdx b/api_docs/kbn_home_sample_data_tab.mdx index 4d716dbf81496..8fbf6eb80ba59 100644 --- a/api_docs/kbn_home_sample_data_tab.mdx +++ b/api_docs/kbn_home_sample_data_tab.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-tab title: "@kbn/home-sample-data-tab" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-tab plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-tab'] --- import kbnHomeSampleDataTabObj from './kbn_home_sample_data_tab.devdocs.json'; diff --git a/api_docs/kbn_i18n.mdx b/api_docs/kbn_i18n.mdx index 6bc49f4c57040..6254e4a7b4362 100644 --- a/api_docs/kbn_i18n.mdx +++ b/api_docs/kbn_i18n.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n title: "@kbn/i18n" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n'] --- import kbnI18nObj from './kbn_i18n.devdocs.json'; diff --git a/api_docs/kbn_i18n_react.mdx b/api_docs/kbn_i18n_react.mdx index b7189e763b31a..3326bcb483f5b 100644 --- a/api_docs/kbn_i18n_react.mdx +++ b/api_docs/kbn_i18n_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n-react title: "@kbn/i18n-react" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n-react plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n-react'] --- import kbnI18nReactObj from './kbn_i18n_react.devdocs.json'; diff --git a/api_docs/kbn_import_resolver.mdx b/api_docs/kbn_import_resolver.mdx index 99b12c47578fb..d36d3d5b24ebb 100644 --- a/api_docs/kbn_import_resolver.mdx +++ b/api_docs/kbn_import_resolver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-import-resolver title: "@kbn/import-resolver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/import-resolver plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/import-resolver'] --- import kbnImportResolverObj from './kbn_import_resolver.devdocs.json'; diff --git a/api_docs/kbn_interpreter.mdx b/api_docs/kbn_interpreter.mdx index 40e1062458edf..69bacaef8152c 100644 --- a/api_docs/kbn_interpreter.mdx +++ b/api_docs/kbn_interpreter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-interpreter title: "@kbn/interpreter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/interpreter plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/interpreter'] --- import kbnInterpreterObj from './kbn_interpreter.devdocs.json'; diff --git a/api_docs/kbn_io_ts_utils.mdx b/api_docs/kbn_io_ts_utils.mdx index 6db565a62a420..577e8c84120cc 100644 --- a/api_docs/kbn_io_ts_utils.mdx +++ b/api_docs/kbn_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-io-ts-utils title: "@kbn/io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/io-ts-utils plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/io-ts-utils'] --- import kbnIoTsUtilsObj from './kbn_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_jest_serializers.mdx b/api_docs/kbn_jest_serializers.mdx index 0fd889f000209..669a905e75fe8 100644 --- a/api_docs/kbn_jest_serializers.mdx +++ b/api_docs/kbn_jest_serializers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-jest-serializers title: "@kbn/jest-serializers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/jest-serializers plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/jest-serializers'] --- import kbnJestSerializersObj from './kbn_jest_serializers.devdocs.json'; diff --git a/api_docs/kbn_journeys.mdx b/api_docs/kbn_journeys.mdx index 68ded90705bf9..798191216c337 100644 --- a/api_docs/kbn_journeys.mdx +++ b/api_docs/kbn_journeys.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-journeys title: "@kbn/journeys" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/journeys plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/journeys'] --- import kbnJourneysObj from './kbn_journeys.devdocs.json'; diff --git a/api_docs/kbn_kibana_manifest_schema.mdx b/api_docs/kbn_kibana_manifest_schema.mdx index c5c7499caab16..41c51da5e6a46 100644 --- a/api_docs/kbn_kibana_manifest_schema.mdx +++ b/api_docs/kbn_kibana_manifest_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-kibana-manifest-schema title: "@kbn/kibana-manifest-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/kibana-manifest-schema plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/kibana-manifest-schema'] --- import kbnKibanaManifestSchemaObj from './kbn_kibana_manifest_schema.devdocs.json'; diff --git a/api_docs/kbn_language_documentation_popover.mdx b/api_docs/kbn_language_documentation_popover.mdx index 038855e8feec6..3354263bd9f3d 100644 --- a/api_docs/kbn_language_documentation_popover.mdx +++ b/api_docs/kbn_language_documentation_popover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-language-documentation-popover title: "@kbn/language-documentation-popover" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/language-documentation-popover plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/language-documentation-popover'] --- import kbnLanguageDocumentationPopoverObj from './kbn_language_documentation_popover.devdocs.json'; diff --git a/api_docs/kbn_logging.mdx b/api_docs/kbn_logging.mdx index 5a0f39e270d14..f56f15e36d5f2 100644 --- a/api_docs/kbn_logging.mdx +++ b/api_docs/kbn_logging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging title: "@kbn/logging" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging'] --- import kbnLoggingObj from './kbn_logging.devdocs.json'; diff --git a/api_docs/kbn_logging_mocks.mdx b/api_docs/kbn_logging_mocks.mdx index 7d1ad8ac6c59f..4beafedafdb2e 100644 --- a/api_docs/kbn_logging_mocks.mdx +++ b/api_docs/kbn_logging_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging-mocks title: "@kbn/logging-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging-mocks'] --- import kbnLoggingMocksObj from './kbn_logging_mocks.devdocs.json'; diff --git a/api_docs/kbn_managed_vscode_config.mdx b/api_docs/kbn_managed_vscode_config.mdx index 022f0ae822ff7..a2cb83db536ed 100644 --- a/api_docs/kbn_managed_vscode_config.mdx +++ b/api_docs/kbn_managed_vscode_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-managed-vscode-config title: "@kbn/managed-vscode-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/managed-vscode-config plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/managed-vscode-config'] --- import kbnManagedVscodeConfigObj from './kbn_managed_vscode_config.devdocs.json'; diff --git a/api_docs/kbn_mapbox_gl.mdx b/api_docs/kbn_mapbox_gl.mdx index e3fc07ecc0e02..eef13984847f4 100644 --- a/api_docs/kbn_mapbox_gl.mdx +++ b/api_docs/kbn_mapbox_gl.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-mapbox-gl title: "@kbn/mapbox-gl" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/mapbox-gl plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/mapbox-gl'] --- import kbnMapboxGlObj from './kbn_mapbox_gl.devdocs.json'; diff --git a/api_docs/kbn_ml_agg_utils.mdx b/api_docs/kbn_ml_agg_utils.mdx index 3441c6bbab497..44d74a00c65d4 100644 --- a/api_docs/kbn_ml_agg_utils.mdx +++ b/api_docs/kbn_ml_agg_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-agg-utils title: "@kbn/ml-agg-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-agg-utils plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-agg-utils'] --- import kbnMlAggUtilsObj from './kbn_ml_agg_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_is_populated_object.mdx b/api_docs/kbn_ml_is_populated_object.mdx index da5e56431bd86..3b002ee5c961e 100644 --- a/api_docs/kbn_ml_is_populated_object.mdx +++ b/api_docs/kbn_ml_is_populated_object.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-populated-object title: "@kbn/ml-is-populated-object" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-populated-object plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-populated-object'] --- import kbnMlIsPopulatedObjectObj from './kbn_ml_is_populated_object.devdocs.json'; diff --git a/api_docs/kbn_ml_string_hash.mdx b/api_docs/kbn_ml_string_hash.mdx index 2b17074ace877..f407debc9be10 100644 --- a/api_docs/kbn_ml_string_hash.mdx +++ b/api_docs/kbn_ml_string_hash.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-string-hash title: "@kbn/ml-string-hash" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-string-hash plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-string-hash'] --- import kbnMlStringHashObj from './kbn_ml_string_hash.devdocs.json'; diff --git a/api_docs/kbn_monaco.devdocs.json b/api_docs/kbn_monaco.devdocs.json index c7fe8ddfb47bf..701fbd8a3f306 100644 --- a/api_docs/kbn_monaco.devdocs.json +++ b/api_docs/kbn_monaco.devdocs.json @@ -724,6 +724,38 @@ } ], "objects": [ + { + "parentPluginId": "@kbn/monaco", + "id": "def-common.monacoeditoresmvseditoreditor.api", + "type": "Object", + "tags": [], + "label": "'monaco-editor/esm/vs/editor/editor.api'", + "description": [], + "signature": [ + "typeof ", + "node_modules/monaco-editor/esm/vs/editor/editor.api" + ], + "path": "node_modules/monaco-yaml/index.d.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/monaco", + "id": "def-common.monacoeditoresmvseditoreditor.api", + "type": "Object", + "tags": [], + "label": "'monaco-editor/esm/vs/editor/editor.api'", + "description": [], + "signature": [ + "typeof ", + "node_modules/monaco-editor/esm/vs/editor/editor.api" + ], + "path": "node_modules/monaco-yaml/index.d.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/monaco", "id": "def-common.PainlessLang", diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index 1be624bd1f9eb..d058a2db34519 100644 --- a/api_docs/kbn_monaco.mdx +++ b/api_docs/kbn_monaco.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-monaco title: "@kbn/monaco" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/monaco plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/monaco'] --- import kbnMonacoObj from './kbn_monaco.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 60 | 0 | 60 | 2 | +| 62 | 0 | 60 | 2 | ## Common diff --git a/api_docs/kbn_optimizer.mdx b/api_docs/kbn_optimizer.mdx index 9bc7303759d95..57a1afe57e7d2 100644 --- a/api_docs/kbn_optimizer.mdx +++ b/api_docs/kbn_optimizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer title: "@kbn/optimizer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer'] --- import kbnOptimizerObj from './kbn_optimizer.devdocs.json'; diff --git a/api_docs/kbn_optimizer_webpack_helpers.mdx b/api_docs/kbn_optimizer_webpack_helpers.mdx index f59a7e1df2e60..9ba139c16a3b6 100644 --- a/api_docs/kbn_optimizer_webpack_helpers.mdx +++ b/api_docs/kbn_optimizer_webpack_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer-webpack-helpers title: "@kbn/optimizer-webpack-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer-webpack-helpers plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer-webpack-helpers'] --- import kbnOptimizerWebpackHelpersObj from './kbn_optimizer_webpack_helpers.devdocs.json'; diff --git a/api_docs/kbn_osquery_io_ts_types.mdx b/api_docs/kbn_osquery_io_ts_types.mdx index 01c51a68e033c..5ab86fba57a7f 100644 --- a/api_docs/kbn_osquery_io_ts_types.mdx +++ b/api_docs/kbn_osquery_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-osquery-io-ts-types title: "@kbn/osquery-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/osquery-io-ts-types plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/osquery-io-ts-types'] --- import kbnOsqueryIoTsTypesObj from './kbn_osquery_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_peggy.mdx b/api_docs/kbn_peggy.mdx index 45d7104ba481b..144efe8f7ab43 100644 --- a/api_docs/kbn_peggy.mdx +++ b/api_docs/kbn_peggy.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-peggy title: "@kbn/peggy" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/peggy plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/peggy'] --- import kbnPeggyObj from './kbn_peggy.devdocs.json'; diff --git a/api_docs/kbn_performance_testing_dataset_extractor.mdx b/api_docs/kbn_performance_testing_dataset_extractor.mdx index 56651bdaa727f..37c5dd2c1824e 100644 --- a/api_docs/kbn_performance_testing_dataset_extractor.mdx +++ b/api_docs/kbn_performance_testing_dataset_extractor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-performance-testing-dataset-extractor title: "@kbn/performance-testing-dataset-extractor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/performance-testing-dataset-extractor plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/performance-testing-dataset-extractor'] --- import kbnPerformanceTestingDatasetExtractorObj from './kbn_performance_testing_dataset_extractor.devdocs.json'; diff --git a/api_docs/kbn_plugin_generator.mdx b/api_docs/kbn_plugin_generator.mdx index 86c771b4f76e0..4ebd3242c1e74 100644 --- a/api_docs/kbn_plugin_generator.mdx +++ b/api_docs/kbn_plugin_generator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-generator title: "@kbn/plugin-generator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-generator plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-generator'] --- import kbnPluginGeneratorObj from './kbn_plugin_generator.devdocs.json'; diff --git a/api_docs/kbn_plugin_helpers.mdx b/api_docs/kbn_plugin_helpers.mdx index fb2f0d3843c7c..735675442233c 100644 --- a/api_docs/kbn_plugin_helpers.mdx +++ b/api_docs/kbn_plugin_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-helpers title: "@kbn/plugin-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-helpers plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-helpers'] --- import kbnPluginHelpersObj from './kbn_plugin_helpers.devdocs.json'; diff --git a/api_docs/kbn_react_field.mdx b/api_docs/kbn_react_field.mdx index 293b61c515d11..1b6186e7befd8 100644 --- a/api_docs/kbn_react_field.mdx +++ b/api_docs/kbn_react_field.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-field title: "@kbn/react-field" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-field plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-field'] --- import kbnReactFieldObj from './kbn_react_field.devdocs.json'; diff --git a/api_docs/kbn_repo_source_classifier.mdx b/api_docs/kbn_repo_source_classifier.mdx index dbfb09ce10d6d..98176c46343fb 100644 --- a/api_docs/kbn_repo_source_classifier.mdx +++ b/api_docs/kbn_repo_source_classifier.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-source-classifier title: "@kbn/repo-source-classifier" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-source-classifier plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-source-classifier'] --- import kbnRepoSourceClassifierObj from './kbn_repo_source_classifier.devdocs.json'; diff --git a/api_docs/kbn_rison.mdx b/api_docs/kbn_rison.mdx index ef1f05d58c4da..2ce5c0a8d79d4 100644 --- a/api_docs/kbn_rison.mdx +++ b/api_docs/kbn_rison.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rison title: "@kbn/rison" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rison plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rison'] --- import kbnRisonObj from './kbn_rison.devdocs.json'; diff --git a/api_docs/kbn_rule_data_utils.mdx b/api_docs/kbn_rule_data_utils.mdx index fdf82c4ebaf55..ff63ef0812fd3 100644 --- a/api_docs/kbn_rule_data_utils.mdx +++ b/api_docs/kbn_rule_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rule-data-utils title: "@kbn/rule-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rule-data-utils plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rule-data-utils'] --- import kbnRuleDataUtilsObj from './kbn_rule_data_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_autocomplete.mdx b/api_docs/kbn_securitysolution_autocomplete.mdx index 0a6d134eb0390..43527221da96e 100644 --- a/api_docs/kbn_securitysolution_autocomplete.mdx +++ b/api_docs/kbn_securitysolution_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-autocomplete title: "@kbn/securitysolution-autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-autocomplete plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-autocomplete'] --- import kbnSecuritysolutionAutocompleteObj from './kbn_securitysolution_autocomplete.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_es_utils.mdx b/api_docs/kbn_securitysolution_es_utils.mdx index cd19859bb8a9d..baec2c99dfdb8 100644 --- a/api_docs/kbn_securitysolution_es_utils.mdx +++ b/api_docs/kbn_securitysolution_es_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-es-utils title: "@kbn/securitysolution-es-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-es-utils plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-es-utils'] --- import kbnSecuritysolutionEsUtilsObj from './kbn_securitysolution_es_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_exception_list_components.devdocs.json b/api_docs/kbn_securitysolution_exception_list_components.devdocs.json index ca9ebec0b58fd..5b3d03d83b8b7 100644 --- a/api_docs/kbn_securitysolution_exception_list_components.devdocs.json +++ b/api_docs/kbn_securitysolution_exception_list_components.devdocs.json @@ -834,7 +834,7 @@ "label": "formattedDateComponent", "description": [], "signature": [ - "\"symbol\" | \"object\" | React.ComponentType | \"body\" | \"path\" | \"circle\" | \"filter\" | \"data\" | \"line\" | \"area\" | \"time\" | \"label\" | \"legend\" | \"image\" | \"link\" | \"menu\" | \"stop\" | \"base\" | \"text\" | \"title\" | \"s\" | \"small\" | \"source\" | \"output\" | \"svg\" | \"meta\" | \"script\" | \"summary\" | \"desc\" | \"q\" | \"pattern\" | \"mask\" | \"input\" | \"slot\" | \"style\" | \"head\" | \"section\" | \"big\" | \"sub\" | \"sup\" | \"animate\" | \"progress\" | \"view\" | \"var\" | \"map\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"form\" | \"main\" | \"abbr\" | \"address\" | \"article\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"canvas\" | \"caption\" | \"cite\" | \"code\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"p\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"select\" | \"span\" | \"strong\" | \"table\" | \"template\" | \"tbody\" | \"td\" | \"textarea\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"clipPath\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" + "\"symbol\" | \"object\" | React.ComponentType | \"body\" | \"path\" | \"circle\" | \"filter\" | \"data\" | \"line\" | \"area\" | \"time\" | \"label\" | \"legend\" | \"article\" | \"image\" | \"link\" | \"menu\" | \"stop\" | \"base\" | \"text\" | \"title\" | \"s\" | \"small\" | \"source\" | \"output\" | \"svg\" | \"meta\" | \"script\" | \"summary\" | \"desc\" | \"q\" | \"pattern\" | \"mask\" | \"input\" | \"slot\" | \"style\" | \"head\" | \"section\" | \"big\" | \"sub\" | \"sup\" | \"animate\" | \"progress\" | \"view\" | \"var\" | \"map\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"form\" | \"main\" | \"abbr\" | \"address\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"canvas\" | \"caption\" | \"cite\" | \"code\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"p\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"select\" | \"span\" | \"strong\" | \"table\" | \"template\" | \"tbody\" | \"td\" | \"textarea\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"clipPath\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" ], "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/index.tsx", "deprecated": false, @@ -848,7 +848,7 @@ "label": "securityLinkAnchorComponent", "description": [], "signature": [ - "\"symbol\" | \"object\" | React.ComponentType | \"body\" | \"path\" | \"circle\" | \"filter\" | \"data\" | \"line\" | \"area\" | \"time\" | \"label\" | \"legend\" | \"image\" | \"link\" | \"menu\" | \"stop\" | \"base\" | \"text\" | \"title\" | \"s\" | \"small\" | \"source\" | \"output\" | \"svg\" | \"meta\" | \"script\" | \"summary\" | \"desc\" | \"q\" | \"pattern\" | \"mask\" | \"input\" | \"slot\" | \"style\" | \"head\" | \"section\" | \"big\" | \"sub\" | \"sup\" | \"animate\" | \"progress\" | \"view\" | \"var\" | \"map\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"form\" | \"main\" | \"abbr\" | \"address\" | \"article\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"canvas\" | \"caption\" | \"cite\" | \"code\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"p\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"select\" | \"span\" | \"strong\" | \"table\" | \"template\" | \"tbody\" | \"td\" | \"textarea\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"clipPath\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" + "\"symbol\" | \"object\" | React.ComponentType | \"body\" | \"path\" | \"circle\" | \"filter\" | \"data\" | \"line\" | \"area\" | \"time\" | \"label\" | \"legend\" | \"article\" | \"image\" | \"link\" | \"menu\" | \"stop\" | \"base\" | \"text\" | \"title\" | \"s\" | \"small\" | \"source\" | \"output\" | \"svg\" | \"meta\" | \"script\" | \"summary\" | \"desc\" | \"q\" | \"pattern\" | \"mask\" | \"input\" | \"slot\" | \"style\" | \"head\" | \"section\" | \"big\" | \"sub\" | \"sup\" | \"animate\" | \"progress\" | \"view\" | \"var\" | \"map\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"form\" | \"main\" | \"abbr\" | \"address\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"canvas\" | \"caption\" | \"cite\" | \"code\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"p\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"select\" | \"span\" | \"strong\" | \"table\" | \"template\" | \"tbody\" | \"td\" | \"textarea\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"clipPath\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" ], "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/meta/index.tsx", "deprecated": false, @@ -987,7 +987,7 @@ "label": "securityLinkAnchorComponent", "description": [], "signature": [ - "\"symbol\" | \"object\" | React.ComponentType | \"body\" | \"path\" | \"circle\" | \"filter\" | \"data\" | \"line\" | \"area\" | \"time\" | \"label\" | \"legend\" | \"image\" | \"link\" | \"menu\" | \"stop\" | \"base\" | \"text\" | \"title\" | \"s\" | \"small\" | \"source\" | \"output\" | \"svg\" | \"meta\" | \"script\" | \"summary\" | \"desc\" | \"q\" | \"pattern\" | \"mask\" | \"input\" | \"slot\" | \"style\" | \"head\" | \"section\" | \"big\" | \"sub\" | \"sup\" | \"animate\" | \"progress\" | \"view\" | \"var\" | \"map\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"form\" | \"main\" | \"abbr\" | \"address\" | \"article\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"canvas\" | \"caption\" | \"cite\" | \"code\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"p\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"select\" | \"span\" | \"strong\" | \"table\" | \"template\" | \"tbody\" | \"td\" | \"textarea\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"clipPath\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" + "\"symbol\" | \"object\" | React.ComponentType | \"body\" | \"path\" | \"circle\" | \"filter\" | \"data\" | \"line\" | \"area\" | \"time\" | \"label\" | \"legend\" | \"article\" | \"image\" | \"link\" | \"menu\" | \"stop\" | \"base\" | \"text\" | \"title\" | \"s\" | \"small\" | \"source\" | \"output\" | \"svg\" | \"meta\" | \"script\" | \"summary\" | \"desc\" | \"q\" | \"pattern\" | \"mask\" | \"input\" | \"slot\" | \"style\" | \"head\" | \"section\" | \"big\" | \"sub\" | \"sup\" | \"animate\" | \"progress\" | \"view\" | \"var\" | \"map\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"form\" | \"main\" | \"abbr\" | \"address\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"canvas\" | \"caption\" | \"cite\" | \"code\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"p\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"select\" | \"span\" | \"strong\" | \"table\" | \"template\" | \"tbody\" | \"td\" | \"textarea\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"clipPath\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" ], "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", "deprecated": false, @@ -1001,7 +1001,7 @@ "label": "formattedDateComponent", "description": [], "signature": [ - "\"symbol\" | \"object\" | React.ComponentType | \"body\" | \"path\" | \"circle\" | \"filter\" | \"data\" | \"line\" | \"area\" | \"time\" | \"label\" | \"legend\" | \"image\" | \"link\" | \"menu\" | \"stop\" | \"base\" | \"text\" | \"title\" | \"s\" | \"small\" | \"source\" | \"output\" | \"svg\" | \"meta\" | \"script\" | \"summary\" | \"desc\" | \"q\" | \"pattern\" | \"mask\" | \"input\" | \"slot\" | \"style\" | \"head\" | \"section\" | \"big\" | \"sub\" | \"sup\" | \"animate\" | \"progress\" | \"view\" | \"var\" | \"map\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"form\" | \"main\" | \"abbr\" | \"address\" | \"article\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"canvas\" | \"caption\" | \"cite\" | \"code\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"p\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"select\" | \"span\" | \"strong\" | \"table\" | \"template\" | \"tbody\" | \"td\" | \"textarea\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"clipPath\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" + "\"symbol\" | \"object\" | React.ComponentType | \"body\" | \"path\" | \"circle\" | \"filter\" | \"data\" | \"line\" | \"area\" | \"time\" | \"label\" | \"legend\" | \"article\" | \"image\" | \"link\" | \"menu\" | \"stop\" | \"base\" | \"text\" | \"title\" | \"s\" | \"small\" | \"source\" | \"output\" | \"svg\" | \"meta\" | \"script\" | \"summary\" | \"desc\" | \"q\" | \"pattern\" | \"mask\" | \"input\" | \"slot\" | \"style\" | \"head\" | \"section\" | \"big\" | \"sub\" | \"sup\" | \"animate\" | \"progress\" | \"view\" | \"var\" | \"map\" | \"html\" | \"a\" | \"img\" | \"audio\" | \"br\" | \"form\" | \"main\" | \"abbr\" | \"address\" | \"aside\" | \"b\" | \"bdi\" | \"bdo\" | \"blockquote\" | \"button\" | \"canvas\" | \"caption\" | \"cite\" | \"code\" | \"col\" | \"colgroup\" | \"datalist\" | \"dd\" | \"del\" | \"details\" | \"dfn\" | \"dialog\" | \"div\" | \"dl\" | \"dt\" | \"em\" | \"embed\" | \"fieldset\" | \"figcaption\" | \"figure\" | \"footer\" | \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"header\" | \"hgroup\" | \"hr\" | \"i\" | \"iframe\" | \"ins\" | \"kbd\" | \"keygen\" | \"li\" | \"mark\" | \"menuitem\" | \"meter\" | \"nav\" | \"noindex\" | \"noscript\" | \"ol\" | \"optgroup\" | \"option\" | \"p\" | \"param\" | \"picture\" | \"pre\" | \"rp\" | \"rt\" | \"ruby\" | \"samp\" | \"select\" | \"span\" | \"strong\" | \"table\" | \"template\" | \"tbody\" | \"td\" | \"textarea\" | \"tfoot\" | \"th\" | \"thead\" | \"tr\" | \"track\" | \"u\" | \"ul\" | \"video\" | \"wbr\" | \"webview\" | \"animateMotion\" | \"animateTransform\" | \"clipPath\" | \"defs\" | \"ellipse\" | \"feBlend\" | \"feColorMatrix\" | \"feComponentTransfer\" | \"feComposite\" | \"feConvolveMatrix\" | \"feDiffuseLighting\" | \"feDisplacementMap\" | \"feDistantLight\" | \"feDropShadow\" | \"feFlood\" | \"feFuncA\" | \"feFuncB\" | \"feFuncG\" | \"feFuncR\" | \"feGaussianBlur\" | \"feImage\" | \"feMerge\" | \"feMergeNode\" | \"feMorphology\" | \"feOffset\" | \"fePointLight\" | \"feSpecularLighting\" | \"feSpotLight\" | \"feTile\" | \"feTurbulence\" | \"foreignObject\" | \"g\" | \"linearGradient\" | \"marker\" | \"metadata\" | \"mpath\" | \"polygon\" | \"polyline\" | \"radialGradient\" | \"rect\" | \"switch\" | \"textPath\" | \"tspan\" | \"use\"" ], "path": "packages/kbn-securitysolution-exception-list-components/src/exception_item_card/exception_item_card.tsx", "deprecated": false, diff --git a/api_docs/kbn_securitysolution_exception_list_components.mdx b/api_docs/kbn_securitysolution_exception_list_components.mdx index 901b07dffb108..bcea39715e135 100644 --- a/api_docs/kbn_securitysolution_exception_list_components.mdx +++ b/api_docs/kbn_securitysolution_exception_list_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-exception-list-components title: "@kbn/securitysolution-exception-list-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-exception-list-components plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-exception-list-components'] --- import kbnSecuritysolutionExceptionListComponentsObj from './kbn_securitysolution_exception_list_components.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_hook_utils.mdx b/api_docs/kbn_securitysolution_hook_utils.mdx index ba704518f3aa4..465691d5b86fd 100644 --- a/api_docs/kbn_securitysolution_hook_utils.mdx +++ b/api_docs/kbn_securitysolution_hook_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-hook-utils title: "@kbn/securitysolution-hook-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-hook-utils plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-hook-utils'] --- import kbnSecuritysolutionHookUtilsObj from './kbn_securitysolution_hook_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx index faa9c0bdcfbd2..5f73636830434 100644 --- a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-alerting-types title: "@kbn/securitysolution-io-ts-alerting-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-alerting-types plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-alerting-types'] --- import kbnSecuritysolutionIoTsAlertingTypesObj from './kbn_securitysolution_io_ts_alerting_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_list_types.mdx b/api_docs/kbn_securitysolution_io_ts_list_types.mdx index bd40712f2549e..e6e66c866b367 100644 --- a/api_docs/kbn_securitysolution_io_ts_list_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_list_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-list-types title: "@kbn/securitysolution-io-ts-list-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-list-types plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-list-types'] --- import kbnSecuritysolutionIoTsListTypesObj from './kbn_securitysolution_io_ts_list_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_types.mdx b/api_docs/kbn_securitysolution_io_ts_types.mdx index 43d49558967ad..c6649588b4af5 100644 --- a/api_docs/kbn_securitysolution_io_ts_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-types title: "@kbn/securitysolution-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-types plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-types'] --- import kbnSecuritysolutionIoTsTypesObj from './kbn_securitysolution_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_utils.mdx b/api_docs/kbn_securitysolution_io_ts_utils.mdx index 2322fdd4bc55e..e6e97b27f19a6 100644 --- a/api_docs/kbn_securitysolution_io_ts_utils.mdx +++ b/api_docs/kbn_securitysolution_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-utils title: "@kbn/securitysolution-io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-utils plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-utils'] --- import kbnSecuritysolutionIoTsUtilsObj from './kbn_securitysolution_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_api.mdx b/api_docs/kbn_securitysolution_list_api.mdx index 4614a604d7ac6..3d398a6a283a4 100644 --- a/api_docs/kbn_securitysolution_list_api.mdx +++ b/api_docs/kbn_securitysolution_list_api.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-api title: "@kbn/securitysolution-list-api" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-api plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-api'] --- import kbnSecuritysolutionListApiObj from './kbn_securitysolution_list_api.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_constants.mdx b/api_docs/kbn_securitysolution_list_constants.mdx index 2f3ca398d0891..42a16657e8882 100644 --- a/api_docs/kbn_securitysolution_list_constants.mdx +++ b/api_docs/kbn_securitysolution_list_constants.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-constants title: "@kbn/securitysolution-list-constants" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-constants plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-constants'] --- import kbnSecuritysolutionListConstantsObj from './kbn_securitysolution_list_constants.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_hooks.mdx b/api_docs/kbn_securitysolution_list_hooks.mdx index f8d217e49d873..1fd720c52d6d4 100644 --- a/api_docs/kbn_securitysolution_list_hooks.mdx +++ b/api_docs/kbn_securitysolution_list_hooks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-hooks title: "@kbn/securitysolution-list-hooks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-hooks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-hooks'] --- import kbnSecuritysolutionListHooksObj from './kbn_securitysolution_list_hooks.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_utils.mdx b/api_docs/kbn_securitysolution_list_utils.mdx index e4e6bbd44d255..515754bbee40e 100644 --- a/api_docs/kbn_securitysolution_list_utils.mdx +++ b/api_docs/kbn_securitysolution_list_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-utils title: "@kbn/securitysolution-list-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-utils plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-utils'] --- import kbnSecuritysolutionListUtilsObj from './kbn_securitysolution_list_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_rules.mdx b/api_docs/kbn_securitysolution_rules.mdx index 8ba4ee8620304..74a7176a01c43 100644 --- a/api_docs/kbn_securitysolution_rules.mdx +++ b/api_docs/kbn_securitysolution_rules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-rules title: "@kbn/securitysolution-rules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-rules plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-rules'] --- import kbnSecuritysolutionRulesObj from './kbn_securitysolution_rules.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_t_grid.mdx b/api_docs/kbn_securitysolution_t_grid.mdx index aa6da391be60c..2ad2b8bbadb74 100644 --- a/api_docs/kbn_securitysolution_t_grid.mdx +++ b/api_docs/kbn_securitysolution_t_grid.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-t-grid title: "@kbn/securitysolution-t-grid" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-t-grid plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-t-grid'] --- import kbnSecuritysolutionTGridObj from './kbn_securitysolution_t_grid.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_utils.mdx b/api_docs/kbn_securitysolution_utils.mdx index 19fe46791acb5..295faf690c1c1 100644 --- a/api_docs/kbn_securitysolution_utils.mdx +++ b/api_docs/kbn_securitysolution_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-utils title: "@kbn/securitysolution-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-utils plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-utils'] --- import kbnSecuritysolutionUtilsObj from './kbn_securitysolution_utils.devdocs.json'; diff --git a/api_docs/kbn_server_http_tools.mdx b/api_docs/kbn_server_http_tools.mdx index 056e7af1b5a87..cc83262b6f389 100644 --- a/api_docs/kbn_server_http_tools.mdx +++ b/api_docs/kbn_server_http_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-http-tools title: "@kbn/server-http-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-http-tools plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-http-tools'] --- import kbnServerHttpToolsObj from './kbn_server_http_tools.devdocs.json'; diff --git a/api_docs/kbn_server_route_repository.mdx b/api_docs/kbn_server_route_repository.mdx index 1df12a9ec37da..f9500769369cd 100644 --- a/api_docs/kbn_server_route_repository.mdx +++ b/api_docs/kbn_server_route_repository.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-route-repository title: "@kbn/server-route-repository" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-route-repository plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-route-repository'] --- import kbnServerRouteRepositoryObj from './kbn_server_route_repository.devdocs.json'; diff --git a/api_docs/kbn_shared_svg.mdx b/api_docs/kbn_shared_svg.mdx index 8bd240a3dafbd..b299fc4cb2def 100644 --- a/api_docs/kbn_shared_svg.mdx +++ b/api_docs/kbn_shared_svg.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-svg title: "@kbn/shared-svg" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-svg plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-svg'] --- import kbnSharedSvgObj from './kbn_shared_svg.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_solution.mdx b/api_docs/kbn_shared_ux_avatar_solution.mdx index b5ce21e93b1a0..746c0dcbef496 100644 --- a/api_docs/kbn_shared_ux_avatar_solution.mdx +++ b/api_docs/kbn_shared_ux_avatar_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-solution title: "@kbn/shared-ux-avatar-solution" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-solution plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-solution'] --- import kbnSharedUxAvatarSolutionObj from './kbn_shared_ux_avatar_solution.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx index e7d472df2430e..af990426ecc90 100644 --- a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx +++ b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-user-profile-components title: "@kbn/shared-ux-avatar-user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-user-profile-components plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-user-profile-components'] --- import kbnSharedUxAvatarUserProfileComponentsObj from './kbn_shared_ux_avatar_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx index 96e2d273b6657..3805dc74f07a7 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen title: "@kbn/shared-ux-button-exit-full-screen" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen'] --- import kbnSharedUxButtonExitFullScreenObj from './kbn_shared_ux_button_exit_full_screen.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx index 4c38237946242..1aee213e04094 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen-mocks title: "@kbn/shared-ux-button-exit-full-screen-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen-mocks'] --- import kbnSharedUxButtonExitFullScreenMocksObj from './kbn_shared_ux_button_exit_full_screen_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_toolbar.mdx b/api_docs/kbn_shared_ux_button_toolbar.mdx index 01ff67d0c8421..aa8a4b8db03a2 100644 --- a/api_docs/kbn_shared_ux_button_toolbar.mdx +++ b/api_docs/kbn_shared_ux_button_toolbar.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-toolbar title: "@kbn/shared-ux-button-toolbar" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-toolbar plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-toolbar'] --- import kbnSharedUxButtonToolbarObj from './kbn_shared_ux_button_toolbar.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data.mdx b/api_docs/kbn_shared_ux_card_no_data.mdx index 343c7d2455377..690c1d5226636 100644 --- a/api_docs/kbn_shared_ux_card_no_data.mdx +++ b/api_docs/kbn_shared_ux_card_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data title: "@kbn/shared-ux-card-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data'] --- import kbnSharedUxCardNoDataObj from './kbn_shared_ux_card_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx index 1a8cb9db55452..05a694295676d 100644 --- a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data-mocks title: "@kbn/shared-ux-card-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data-mocks'] --- import kbnSharedUxCardNoDataMocksObj from './kbn_shared_ux_card_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_context.mdx b/api_docs/kbn_shared_ux_file_context.mdx index 88cdd9329013f..614944ee0776e 100644 --- a/api_docs/kbn_shared_ux_file_context.mdx +++ b/api_docs/kbn_shared_ux_file_context.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-context title: "@kbn/shared-ux-file-context" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-context plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-context'] --- import kbnSharedUxFileContextObj from './kbn_shared_ux_file_context.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image.mdx b/api_docs/kbn_shared_ux_file_image.mdx index f6c3b2222f10d..712b8f69ac69e 100644 --- a/api_docs/kbn_shared_ux_file_image.mdx +++ b/api_docs/kbn_shared_ux_file_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image title: "@kbn/shared-ux-file-image" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image'] --- import kbnSharedUxFileImageObj from './kbn_shared_ux_file_image.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image_mocks.mdx b/api_docs/kbn_shared_ux_file_image_mocks.mdx index d352c6a294d0b..88d3619962a5a 100644 --- a/api_docs/kbn_shared_ux_file_image_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_image_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image-mocks title: "@kbn/shared-ux-file-image-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image-mocks'] --- import kbnSharedUxFileImageMocksObj from './kbn_shared_ux_file_image_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_mocks.mdx b/api_docs/kbn_shared_ux_file_mocks.mdx index 10f021f630d81..83fd3933fb12a 100644 --- a/api_docs/kbn_shared_ux_file_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-mocks title: "@kbn/shared-ux-file-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-mocks'] --- import kbnSharedUxFileMocksObj from './kbn_shared_ux_file_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_picker.devdocs.json b/api_docs/kbn_shared_ux_file_picker.devdocs.json new file mode 100644 index 0000000000000..ccc2cd14e2d56 --- /dev/null +++ b/api_docs/kbn_shared_ux_file_picker.devdocs.json @@ -0,0 +1,255 @@ +{ + "id": "@kbn/shared-ux-file-picker", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/shared-ux-file-picker", + "id": "def-common.FilePicker", + "type": "Function", + "tags": [], + "label": "FilePicker", + "description": [], + "signature": [ + "(props: ", + { + "pluginId": "@kbn/shared-ux-file-picker", + "scope": "common", + "docId": "kibKbnSharedUxFilePickerPluginApi", + "section": "def-common.Props", + "text": "Props" + }, + ") => JSX.Element" + ], + "path": "packages/shared-ux/file/file_picker/impl/src/index.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/shared-ux-file-picker", + "id": "def-common.FilePicker.$1", + "type": "Object", + "tags": [], + "label": "props", + "description": [], + "signature": [ + { + "pluginId": "@kbn/shared-ux-file-picker", + "scope": "common", + "docId": "kibKbnSharedUxFilePickerPluginApi", + "section": "def-common.Props", + "text": "Props" + }, + "" + ], + "path": "packages/shared-ux/file/file_picker/impl/src/index.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [ + { + "parentPluginId": "@kbn/shared-ux-file-picker", + "id": "def-common.Props", + "type": "Interface", + "tags": [], + "label": "Props", + "description": [], + "signature": [ + { + "pluginId": "@kbn/shared-ux-file-picker", + "scope": "common", + "docId": "kibKbnSharedUxFilePickerPluginApi", + "section": "def-common.Props", + "text": "Props" + }, + "" + ], + "path": "packages/shared-ux/file/file_picker/impl/src/file_picker.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/shared-ux-file-picker", + "id": "def-common.Props.kind", + "type": "Uncategorized", + "tags": [], + "label": "kind", + "description": [ + "\nThe file kind that was passed to the registry." + ], + "signature": [ + "Kind" + ], + "path": "packages/shared-ux/file/file_picker/impl/src/file_picker.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/shared-ux-file-picker", + "id": "def-common.Props.onClose", + "type": "Function", + "tags": [], + "label": "onClose", + "description": [ + "\nWill be called when the modal is closed" + ], + "signature": [ + "() => void" + ], + "path": "packages/shared-ux/file/file_picker/impl/src/file_picker.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/shared-ux-file-picker", + "id": "def-common.Props.onDone", + "type": "Function", + "tags": [], + "label": "onDone", + "description": [ + "\nWill be called after a user has a selected a set of files" + ], + "signature": [ + "(files: ", + "FileJSON", + "[]) => void" + ], + "path": "packages/shared-ux/file/file_picker/impl/src/file_picker.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/shared-ux-file-picker", + "id": "def-common.Props.onDone.$1", + "type": "Array", + "tags": [], + "label": "files", + "description": [], + "signature": [ + "FileJSON", + "[]" + ], + "path": "packages/shared-ux/file/file_picker/impl/src/file_picker.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/shared-ux-file-picker", + "id": "def-common.Props.onUpload", + "type": "Function", + "tags": [], + "label": "onUpload", + "description": [ + "\nWhen a user has successfully uploaded some files this callback will be called" + ], + "signature": [ + "((done: ", + { + "pluginId": "@kbn/shared-ux-file-upload", + "scope": "common", + "docId": "kibKbnSharedUxFileUploadPluginApi", + "section": "def-common.DoneNotification", + "text": "DoneNotification" + }, + "[]) => void) | undefined" + ], + "path": "packages/shared-ux/file/file_picker/impl/src/file_picker.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/shared-ux-file-picker", + "id": "def-common.Props.onUpload.$1", + "type": "Array", + "tags": [], + "label": "done", + "description": [], + "signature": [ + { + "pluginId": "@kbn/shared-ux-file-upload", + "scope": "common", + "docId": "kibKbnSharedUxFileUploadPluginApi", + "section": "def-common.DoneNotification", + "text": "DoneNotification" + }, + "[]" + ], + "path": "packages/shared-ux/file/file_picker/impl/src/file_picker.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/shared-ux-file-picker", + "id": "def-common.Props.pageSize", + "type": "number", + "tags": [], + "label": "pageSize", + "description": [ + "\nThe number of results to show per page." + ], + "signature": [ + "number | undefined" + ], + "path": "packages/shared-ux/file/file_picker/impl/src/file_picker.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/shared-ux-file-picker", + "id": "def-common.Props.multiple", + "type": "CompoundType", + "tags": [ + "default" + ], + "label": "multiple", + "description": [ + "\nWhether you can select one or more files\n" + ], + "signature": [ + "boolean | undefined" + ], + "path": "packages/shared-ux/file/file_picker/impl/src/file_picker.tsx", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_shared_ux_file_picker.mdx b/api_docs/kbn_shared_ux_file_picker.mdx new file mode 100644 index 0000000000000..6a5544d3f60a8 --- /dev/null +++ b/api_docs/kbn_shared_ux_file_picker.mdx @@ -0,0 +1,33 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnSharedUxFilePickerPluginApi +slug: /kibana-dev-docs/api/kbn-shared-ux-file-picker +title: "@kbn/shared-ux-file-picker" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/shared-ux-file-picker plugin +date: 2022-12-20 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-picker'] +--- +import kbnSharedUxFilePickerObj from './kbn_shared_ux_file_picker.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 11 | 0 | 5 | 0 | + +## Common + +### Functions + + +### Interfaces + + diff --git a/api_docs/kbn_shared_ux_file_upload.devdocs.json b/api_docs/kbn_shared_ux_file_upload.devdocs.json new file mode 100644 index 0000000000000..143e11c571ef2 --- /dev/null +++ b/api_docs/kbn_shared_ux_file_upload.devdocs.json @@ -0,0 +1,154 @@ +{ + "id": "@kbn/shared-ux-file-upload", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/shared-ux-file-upload", + "id": "def-common.FileUpload", + "type": "Function", + "tags": [], + "label": "FileUpload", + "description": [], + "signature": [ + "(props: ", + { + "pluginId": "@kbn/shared-ux-file-upload", + "scope": "common", + "docId": "kibKbnSharedUxFileUploadPluginApi", + "section": "def-common.FileUploadProps", + "text": "FileUploadProps" + }, + ") => JSX.Element" + ], + "path": "packages/shared-ux/file/file_upload/impl/src/index.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/shared-ux-file-upload", + "id": "def-common.FileUpload.$1", + "type": "CompoundType", + "tags": [], + "label": "props", + "description": [], + "signature": [ + { + "pluginId": "@kbn/shared-ux-file-upload", + "scope": "common", + "docId": "kibKbnSharedUxFileUploadPluginApi", + "section": "def-common.FileUploadProps", + "text": "FileUploadProps" + } + ], + "path": "packages/shared-ux/file/file_upload/impl/src/index.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [ + { + "parentPluginId": "@kbn/shared-ux-file-upload", + "id": "def-common.DoneNotification", + "type": "Interface", + "tags": [], + "label": "DoneNotification", + "description": [], + "signature": [ + { + "pluginId": "@kbn/shared-ux-file-upload", + "scope": "common", + "docId": "kibKbnSharedUxFileUploadPluginApi", + "section": "def-common.DoneNotification", + "text": "DoneNotification" + }, + "" + ], + "path": "packages/shared-ux/file/file_upload/impl/src/upload_state.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/shared-ux-file-upload", + "id": "def-common.DoneNotification.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "path": "packages/shared-ux/file/file_upload/impl/src/upload_state.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/shared-ux-file-upload", + "id": "def-common.DoneNotification.kind", + "type": "string", + "tags": [], + "label": "kind", + "description": [], + "path": "packages/shared-ux/file/file_upload/impl/src/upload_state.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/shared-ux-file-upload", + "id": "def-common.DoneNotification.fileJSON", + "type": "Object", + "tags": [], + "label": "fileJSON", + "description": [], + "signature": [ + "FileJSON", + "" + ], + "path": "packages/shared-ux/file/file_upload/impl/src/upload_state.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/shared-ux-file-upload", + "id": "def-common.FileUploadProps", + "type": "Type", + "tags": [], + "label": "FileUploadProps", + "description": [], + "signature": [ + "Props", + " & { lazyLoadFallback?: React.ReactNode; }" + ], + "path": "packages/shared-ux/file/file_upload/impl/src/index.tsx", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_shared_ux_file_upload.mdx b/api_docs/kbn_shared_ux_file_upload.mdx new file mode 100644 index 0000000000000..5606b34f17004 --- /dev/null +++ b/api_docs/kbn_shared_ux_file_upload.mdx @@ -0,0 +1,36 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnSharedUxFileUploadPluginApi +slug: /kibana-dev-docs/api/kbn-shared-ux-file-upload +title: "@kbn/shared-ux-file-upload" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/shared-ux-file-upload plugin +date: 2022-12-20 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-upload'] +--- +import kbnSharedUxFileUploadObj from './kbn_shared_ux_file_upload.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 7 | 0 | 7 | 1 | + +## Common + +### Functions + + +### Interfaces + + +### Consts, variables and types + + diff --git a/api_docs/kbn_shared_ux_file_util.mdx b/api_docs/kbn_shared_ux_file_util.mdx index 27d117fca04f3..2e99632d1e0ac 100644 --- a/api_docs/kbn_shared_ux_file_util.mdx +++ b/api_docs/kbn_shared_ux_file_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-util title: "@kbn/shared-ux-file-util" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-util plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-util'] --- import kbnSharedUxFileUtilObj from './kbn_shared_ux_file_util.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app.mdx b/api_docs/kbn_shared_ux_link_redirect_app.mdx index ac700a7bedfb9..9dd152c58d804 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app title: "@kbn/shared-ux-link-redirect-app" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app'] --- import kbnSharedUxLinkRedirectAppObj from './kbn_shared_ux_link_redirect_app.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx index feea6e1a141cc..4865b7287e70c 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app-mocks title: "@kbn/shared-ux-link-redirect-app-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app-mocks'] --- import kbnSharedUxLinkRedirectAppMocksObj from './kbn_shared_ux_link_redirect_app_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown.mdx b/api_docs/kbn_shared_ux_markdown.mdx index 23e93ec55ea29..5f7740875671f 100644 --- a/api_docs/kbn_shared_ux_markdown.mdx +++ b/api_docs/kbn_shared_ux_markdown.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown title: "@kbn/shared-ux-markdown" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown'] --- import kbnSharedUxMarkdownObj from './kbn_shared_ux_markdown.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown_mocks.mdx b/api_docs/kbn_shared_ux_markdown_mocks.mdx index 2166c49a80530..6e949b58a5d9a 100644 --- a/api_docs/kbn_shared_ux_markdown_mocks.mdx +++ b/api_docs/kbn_shared_ux_markdown_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown-mocks title: "@kbn/shared-ux-markdown-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown-mocks'] --- import kbnSharedUxMarkdownMocksObj from './kbn_shared_ux_markdown_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx index 3ff966b6b56b3..cc3735020bfcc 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data title: "@kbn/shared-ux-page-analytics-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data'] --- import kbnSharedUxPageAnalyticsNoDataObj from './kbn_shared_ux_page_analytics_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx index 95c559adb248b..2c80c00a6c5b7 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data-mocks title: "@kbn/shared-ux-page-analytics-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data-mocks'] --- import kbnSharedUxPageAnalyticsNoDataMocksObj from './kbn_shared_ux_page_analytics_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx index b34d7f51a0bef..80fe6c2a779cf 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data title: "@kbn/shared-ux-page-kibana-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data'] --- import kbnSharedUxPageKibanaNoDataObj from './kbn_shared_ux_page_kibana_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx index 62c8ccb60a3b1..c49323267e57b 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data-mocks title: "@kbn/shared-ux-page-kibana-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data-mocks'] --- import kbnSharedUxPageKibanaNoDataMocksObj from './kbn_shared_ux_page_kibana_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template.mdx b/api_docs/kbn_shared_ux_page_kibana_template.mdx index c2a0e4bbd01df..e8076f47f5ff0 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template title: "@kbn/shared-ux-page-kibana-template" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template'] --- import kbnSharedUxPageKibanaTemplateObj from './kbn_shared_ux_page_kibana_template.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx index 4cb982e3273fe..045e1e3378ba5 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template-mocks title: "@kbn/shared-ux-page-kibana-template-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template-mocks'] --- import kbnSharedUxPageKibanaTemplateMocksObj from './kbn_shared_ux_page_kibana_template_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data.mdx b/api_docs/kbn_shared_ux_page_no_data.mdx index cae30e936a812..7ee48c676ff5f 100644 --- a/api_docs/kbn_shared_ux_page_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data title: "@kbn/shared-ux-page-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data'] --- import kbnSharedUxPageNoDataObj from './kbn_shared_ux_page_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config.mdx b/api_docs/kbn_shared_ux_page_no_data_config.mdx index 4bc9d6c326d3b..fd5c65d7868fd 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config title: "@kbn/shared-ux-page-no-data-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config'] --- import kbnSharedUxPageNoDataConfigObj from './kbn_shared_ux_page_no_data_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx index 62b4f763f5048..3b538a43914cd 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config-mocks title: "@kbn/shared-ux-page-no-data-config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config-mocks'] --- import kbnSharedUxPageNoDataConfigMocksObj from './kbn_shared_ux_page_no_data_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx index a040de01964cf..7446f633cb9e6 100644 --- a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-mocks title: "@kbn/shared-ux-page-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-mocks'] --- import kbnSharedUxPageNoDataMocksObj from './kbn_shared_ux_page_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_solution_nav.mdx b/api_docs/kbn_shared_ux_page_solution_nav.mdx index de1a3fa83a89b..4b7fb0c17e21f 100644 --- a/api_docs/kbn_shared_ux_page_solution_nav.mdx +++ b/api_docs/kbn_shared_ux_page_solution_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-solution-nav title: "@kbn/shared-ux-page-solution-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-solution-nav plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-solution-nav'] --- import kbnSharedUxPageSolutionNavObj from './kbn_shared_ux_page_solution_nav.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx index 0f512561cb2cf..58f1922addb85 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views title: "@kbn/shared-ux-prompt-no-data-views" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views'] --- import kbnSharedUxPromptNoDataViewsObj from './kbn_shared_ux_prompt_no_data_views.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx index ac95aba4ab510..c38e30b44c5fb 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views-mocks title: "@kbn/shared-ux-prompt-no-data-views-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views-mocks'] --- import kbnSharedUxPromptNoDataViewsMocksObj from './kbn_shared_ux_prompt_no_data_views_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_not_found.mdx b/api_docs/kbn_shared_ux_prompt_not_found.mdx index 9d65c5285faac..9bd4efaae8e5d 100644 --- a/api_docs/kbn_shared_ux_prompt_not_found.mdx +++ b/api_docs/kbn_shared_ux_prompt_not_found.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-not-found title: "@kbn/shared-ux-prompt-not-found" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-not-found plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-not-found'] --- import kbnSharedUxPromptNotFoundObj from './kbn_shared_ux_prompt_not_found.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router.mdx b/api_docs/kbn_shared_ux_router.mdx index 245a31755891c..9d36a1102bd3b 100644 --- a/api_docs/kbn_shared_ux_router.mdx +++ b/api_docs/kbn_shared_ux_router.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router title: "@kbn/shared-ux-router" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router'] --- import kbnSharedUxRouterObj from './kbn_shared_ux_router.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router_mocks.mdx b/api_docs/kbn_shared_ux_router_mocks.mdx index b3715a2c47404..88d8622c00b89 100644 --- a/api_docs/kbn_shared_ux_router_mocks.mdx +++ b/api_docs/kbn_shared_ux_router_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router-mocks title: "@kbn/shared-ux-router-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router-mocks plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router-mocks'] --- import kbnSharedUxRouterMocksObj from './kbn_shared_ux_router_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_config.mdx b/api_docs/kbn_shared_ux_storybook_config.mdx index 6bb393de688f6..d05db960c3e39 100644 --- a/api_docs/kbn_shared_ux_storybook_config.mdx +++ b/api_docs/kbn_shared_ux_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-config title: "@kbn/shared-ux-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-config plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-config'] --- import kbnSharedUxStorybookConfigObj from './kbn_shared_ux_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_mock.mdx b/api_docs/kbn_shared_ux_storybook_mock.mdx index aefe3c6212c12..db3aaae117eb7 100644 --- a/api_docs/kbn_shared_ux_storybook_mock.mdx +++ b/api_docs/kbn_shared_ux_storybook_mock.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-mock title: "@kbn/shared-ux-storybook-mock" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-mock plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-mock'] --- import kbnSharedUxStorybookMockObj from './kbn_shared_ux_storybook_mock.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_utility.mdx b/api_docs/kbn_shared_ux_utility.mdx index d13f831531fc7..b1d47e5bbbce2 100644 --- a/api_docs/kbn_shared_ux_utility.mdx +++ b/api_docs/kbn_shared_ux_utility.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-utility title: "@kbn/shared-ux-utility" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-utility plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-utility'] --- import kbnSharedUxUtilityObj from './kbn_shared_ux_utility.devdocs.json'; diff --git a/api_docs/kbn_some_dev_log.mdx b/api_docs/kbn_some_dev_log.mdx index 31153cfed22ae..79c018ece8760 100644 --- a/api_docs/kbn_some_dev_log.mdx +++ b/api_docs/kbn_some_dev_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-some-dev-log title: "@kbn/some-dev-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/some-dev-log plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/some-dev-log'] --- import kbnSomeDevLogObj from './kbn_some_dev_log.devdocs.json'; diff --git a/api_docs/kbn_sort_package_json.mdx b/api_docs/kbn_sort_package_json.mdx index f546c7979c3ad..c0da01f7d99da 100644 --- a/api_docs/kbn_sort_package_json.mdx +++ b/api_docs/kbn_sort_package_json.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-sort-package-json title: "@kbn/sort-package-json" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/sort-package-json plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sort-package-json'] --- import kbnSortPackageJsonObj from './kbn_sort_package_json.devdocs.json'; diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx index 73db87176f065..a7d65b98853d1 100644 --- a/api_docs/kbn_std.mdx +++ b/api_docs/kbn_std.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-std title: "@kbn/std" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/std plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/std'] --- import kbnStdObj from './kbn_std.devdocs.json'; diff --git a/api_docs/kbn_stdio_dev_helpers.mdx b/api_docs/kbn_stdio_dev_helpers.mdx index ae89f4a2dbaff..fd4cc9a582f1a 100644 --- a/api_docs/kbn_stdio_dev_helpers.mdx +++ b/api_docs/kbn_stdio_dev_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-stdio-dev-helpers title: "@kbn/stdio-dev-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/stdio-dev-helpers plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/stdio-dev-helpers'] --- import kbnStdioDevHelpersObj from './kbn_stdio_dev_helpers.devdocs.json'; diff --git a/api_docs/kbn_storybook.mdx b/api_docs/kbn_storybook.mdx index edd6f369f6c4c..36372a3abe910 100644 --- a/api_docs/kbn_storybook.mdx +++ b/api_docs/kbn_storybook.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-storybook title: "@kbn/storybook" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/storybook plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/storybook'] --- import kbnStorybookObj from './kbn_storybook.devdocs.json'; diff --git a/api_docs/kbn_telemetry_tools.mdx b/api_docs/kbn_telemetry_tools.mdx index 33fb5832312ae..44fc9424f54c8 100644 --- a/api_docs/kbn_telemetry_tools.mdx +++ b/api_docs/kbn_telemetry_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-telemetry-tools title: "@kbn/telemetry-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/telemetry-tools plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/telemetry-tools'] --- import kbnTelemetryToolsObj from './kbn_telemetry_tools.devdocs.json'; diff --git a/api_docs/kbn_test.mdx b/api_docs/kbn_test.mdx index b22dfd8cbb80e..db16aca193c9e 100644 --- a/api_docs/kbn_test.mdx +++ b/api_docs/kbn_test.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test title: "@kbn/test" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test'] --- import kbnTestObj from './kbn_test.devdocs.json'; diff --git a/api_docs/kbn_test_jest_helpers.mdx b/api_docs/kbn_test_jest_helpers.mdx index 2af333c5df418..cb6f5ac0e238e 100644 --- a/api_docs/kbn_test_jest_helpers.mdx +++ b/api_docs/kbn_test_jest_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-jest-helpers title: "@kbn/test-jest-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-jest-helpers plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-jest-helpers'] --- import kbnTestJestHelpersObj from './kbn_test_jest_helpers.devdocs.json'; diff --git a/api_docs/kbn_test_subj_selector.mdx b/api_docs/kbn_test_subj_selector.mdx index 102939154a183..b4ef7c962454f 100644 --- a/api_docs/kbn_test_subj_selector.mdx +++ b/api_docs/kbn_test_subj_selector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-subj-selector title: "@kbn/test-subj-selector" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-subj-selector plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-subj-selector'] --- import kbnTestSubjSelectorObj from './kbn_test_subj_selector.devdocs.json'; diff --git a/api_docs/kbn_tooling_log.mdx b/api_docs/kbn_tooling_log.mdx index 10bcb686fe5e9..d622fde8b9cf6 100644 --- a/api_docs/kbn_tooling_log.mdx +++ b/api_docs/kbn_tooling_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-tooling-log title: "@kbn/tooling-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/tooling-log plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log'] --- import kbnToolingLogObj from './kbn_tooling_log.devdocs.json'; diff --git a/api_docs/kbn_type_summarizer.mdx b/api_docs/kbn_type_summarizer.mdx index 8c3a086534bdb..ea502a5501a88 100644 --- a/api_docs/kbn_type_summarizer.mdx +++ b/api_docs/kbn_type_summarizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-type-summarizer title: "@kbn/type-summarizer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/type-summarizer plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/type-summarizer'] --- import kbnTypeSummarizerObj from './kbn_type_summarizer.devdocs.json'; diff --git a/api_docs/kbn_type_summarizer_core.mdx b/api_docs/kbn_type_summarizer_core.mdx index d161f612db9d3..04813b8643c80 100644 --- a/api_docs/kbn_type_summarizer_core.mdx +++ b/api_docs/kbn_type_summarizer_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-type-summarizer-core title: "@kbn/type-summarizer-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/type-summarizer-core plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/type-summarizer-core'] --- import kbnTypeSummarizerCoreObj from './kbn_type_summarizer_core.devdocs.json'; diff --git a/api_docs/kbn_typed_react_router_config.mdx b/api_docs/kbn_typed_react_router_config.mdx index e53680ec54f67..0f70f70cdc906 100644 --- a/api_docs/kbn_typed_react_router_config.mdx +++ b/api_docs/kbn_typed_react_router_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-typed-react-router-config title: "@kbn/typed-react-router-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/typed-react-router-config plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/typed-react-router-config'] --- import kbnTypedReactRouterConfigObj from './kbn_typed_react_router_config.devdocs.json'; diff --git a/api_docs/kbn_ui_shared_deps_src.mdx b/api_docs/kbn_ui_shared_deps_src.mdx index 41406cb76bdbd..b87f686dc4804 100644 --- a/api_docs/kbn_ui_shared_deps_src.mdx +++ b/api_docs/kbn_ui_shared_deps_src.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-shared-deps-src title: "@kbn/ui-shared-deps-src" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-shared-deps-src plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-shared-deps-src'] --- import kbnUiSharedDepsSrcObj from './kbn_ui_shared_deps_src.devdocs.json'; diff --git a/api_docs/kbn_ui_theme.devdocs.json b/api_docs/kbn_ui_theme.devdocs.json index 015bf81abefa7..5d5a8c70282ca 100644 --- a/api_docs/kbn_ui_theme.devdocs.json +++ b/api_docs/kbn_ui_theme.devdocs.json @@ -54,7 +54,7 @@ "label": "Theme", "description": [], "signature": [ - "{ euiCollapsibleNavGroupLightBackgroundColor: string; euiCollapsibleNavGroupDarkBackgroundColor: string; euiCollapsibleNavGroupDarkHighContrastColor: string; euiColorPickerValueRange0: string; euiColorPickerValueRange1: string; euiColorPickerSaturationRange0: string; euiColorPickerSaturationRange1: string; euiColorPickerIndicatorSize: string; euiColorPickerWidth: string; euiColorPaletteDisplaySizes: { sizeExtraSmall: string; sizeSmall: string; sizeMedium: string; }; euiContextMenuWidth: string; euiControlBarBackground: string; euiControlBarText: string; euiControlBarBorderColor: string; euiControlBarInitialHeight: string; euiControlBarMaxHeight: string; euiControlBarHeights: { s: string; m: string; l: string; }; euiDataGridPrefix: string; euiDataGridStyles: string; euiZDataGrid: number; euiZHeaderBelowDataGrid: number; euiZDataGridCellPopover: number; euiDataGridColumnResizerWidth: string; euiDataGridPopoverMaxHeight: string; euiDataGridCellPaddingS: string; euiDataGridCellPaddingM: string; euiDataGridCellPaddingL: string; euiDataGridVerticalBorder: string; euiSuperDatePickerWidth: string; euiSuperDatePickerButtonWidth: string; euiDragAndDropSpacing: { s: string; m: string; l: string; }; euiEmptyPromptContentMaxWidth: string; euiFilePickerTallHeight: string; euiRangeLevelColors: { primary: string; success: string; warning: string; danger: string; }; textareaResizing: { vertical: string; horizontal: string; both: string; none: string; }; euiHeaderLinksGutterSizes: { gutterXS: string; gutterS: string; gutterM: string; gutterL: string; }; euiKeyPadMenuSize: string; euiKeyPadMenuMarginSize: string; euiMarkdownEditorMinHeight: string; euiResizableButtonTransitionSpeed: string; euiResizableButtonSize: string; euiSelectableListItemBorder: string; euiSelectableListItemPadding: string; euiSelectableTemplateSitewideTypes: { application: { color: string; 'font-weight': number; }; deployment: { color: string; 'font-weight': number; }; article: { color: string; 'font-weight': number; }; case: { color: string; 'font-weight': number; }; platform: { color: string; 'font-weight': number; }; }; euiSideNavEmphasizedBackgroundColor: string; euiSideNavRootTextcolor: string; euiSideNavBranchTextcolor: string; euiSideNavSelectedTextcolor: string; euiSideNavDisabledTextcolor: string; euiStepNumberSize: string; euiStepNumberSmallSize: string; euiStepNumberMargin: string; euiStepStatusColorsToFade: { warning: string; danger: string; disabled: string; incomplete: string; }; euiSuggestItemColors: { tint0: string; tint1: string; tint2: string; tint3: string; tint4: string; tint5: string; tint6: string; tint7: string; tint8: string; tint9: string; tint10: string; }; euiTableCellContentPadding: string; euiTableCellContentPaddingCompressed: string; euiTableCellCheckboxWidth: string; euiTableActionsAreaWidth: string; euiTableHoverColor: string; euiTableSelectedColor: string; euiTableHoverSelectedColor: string; euiTableActionsBorderColor: string; euiTableHoverClickableColor: string; euiTableFocusClickableColor: string; euiContrastRatioText: number; euiContrastRatioGraphic: number; euiContrastRatioDisabled: number; euiAnimSlightBounce: string; euiAnimSlightResistance: string; euiAnimSpeedExtraFast: string; euiAnimSpeedFast: string; euiAnimSpeedNormal: string; euiAnimSpeedSlow: string; euiAnimSpeedExtraSlow: string; euiBorderWidthThin: string; euiBorderWidthThick: string; euiBorderColor: string; euiBorderRadius: string; euiBorderRadiusSmall: string; euiBorderThick: string; euiBorderThin: string; euiBorderEditable: string; euiButtonHeight: string; euiButtonHeightSmall: string; euiButtonHeightXSmall: string; euiButtonColorDisabled: string; euiButtonColorDisabledText: string; euiButtonColorGhostDisabled: string; euiButtonTypes: { primary: string; accent: string; success: string; warning: string; danger: string; ghost: string; text: string; }; euiPaletteColorBlind: { euiColorVis0: { graphic: string; behindText: string; }; euiColorVis1: { graphic: string; behindText: string; }; euiColorVis2: { graphic: string; behindText: string; }; euiColorVis3: { graphic: string; behindText: string; }; euiColorVis4: { graphic: string; behindText: string; }; euiColorVis5: { graphic: string; behindText: string; }; euiColorVis6: { graphic: string; behindText: string; }; euiColorVis7: { graphic: string; behindText: string; }; euiColorVis8: { graphic: string; behindText: string; }; euiColorVis9: { graphic: string; behindText: string; }; }; euiPaletteColorBlindKeys: string; euiColorVis0: string; euiColorVis1: string; euiColorVis2: string; euiColorVis3: string; euiColorVis4: string; euiColorVis5: string; euiColorVis6: string; euiColorVis7: string; euiColorVis8: string; euiColorVis9: string; euiColorVis0_behindText: string; euiColorVis1_behindText: string; euiColorVis2_behindText: string; euiColorVis3_behindText: string; euiColorVis4_behindText: string; euiColorVis5_behindText: string; euiColorVis6_behindText: string; euiColorVis7_behindText: string; euiColorVis8_behindText: string; euiColorVis9_behindText: string; euiFontWeightLight: number; euiFontWeightRegular: number; euiFontWeightMedium: number; euiFontWeightSemiBold: number; euiFontWeightBold: number; euiCodeFontWeightRegular: number; euiCodeFontWeightBold: number; euiFormMaxWidth: string; euiFormControlHeight: string; euiFormControlCompressedHeight: string; euiFormControlPadding: string; euiFormControlCompressedPadding: string; euiFormControlBorderRadius: string; euiFormControlCompressedBorderRadius: string; euiRadioSize: string; euiCheckBoxSize: string; euiCheckboxBorderRadius: string; euiSwitchHeight: string; euiSwitchWidth: string; euiSwitchThumbSize: string; euiSwitchIconHeight: string; euiSwitchHeightCompressed: string; euiSwitchWidthCompressed: string; euiSwitchThumbSizeCompressed: string; euiSwitchHeightMini: string; euiSwitchWidthMini: string; euiSwitchThumbSizeMini: string; euiFormBackgroundColor: string; euiFormBackgroundDisabledColor: string; euiFormBackgroundReadOnlyColor: string; euiFormBorderOpaqueColor: string; euiFormBorderColor: string; euiFormBorderDisabledColor: string; euiFormCustomControlDisabledIconColor: string; euiFormCustomControlBorderColor: string; euiFormControlDisabledColor: string; euiFormControlBoxShadow: string; euiFormControlPlaceholderText: string; euiFormInputGroupLabelBackground: string; euiFormInputGroupBorder: string; euiSwitchOffColor: string; euiFormControlIconSizes: { small: string; medium: string; large: string; xLarge: string; xxLarge: string; }; euiFormControlLayoutGroupInputHeight: string; euiFormControlLayoutGroupInputCompressedHeight: string; euiFormControlLayoutGroupInputCompressedBorderRadius: string; euiRangeTrackColor: string; euiRangeHighlightColor: string; euiRangeThumbRadius: string; euiRangeThumbHeight: string; euiRangeThumbWidth: string; euiRangeThumbBorderColor: string; euiRangeThumbBackgroundColor: string; euiRangeTrackWidth: string; euiRangeTrackHeight: string; euiRangeTrackCompressedHeight: string; euiRangeTrackBorderWidth: number; euiRangeTrackBorderColor: string; euiRangeTrackRadius: string; euiRangeDisabledOpacity: number; euiRangeHighlightHeight: string; euiRangeHighlightCompressedHeight: string; euiRangeHeight: string; euiRangeCompressedHeight: string; euiHeaderBackgroundColor: string; euiHeaderDarkBackgroundColor: string; euiHeaderBorderColor: string; euiHeaderBreadcrumbColor: string; euiHeaderHeight: string; euiHeaderChildSize: string; euiHeaderHeightCompensation: string; euiPageDefaultMaxWidth: string; euiPageSidebarMinWidth: string; euiPanelPaddingModifiers: { paddingSmall: string; paddingMedium: string; paddingLarge: string; }; euiPanelBorderRadiusModifiers: { borderRadiusNone: number; borderRadiusMedium: string; }; euiPanelBackgroundColorModifiers: { transparent: string; plain: string; subdued: string; accent: string; primary: string; success: string; warning: string; danger: string; }; euiBreakpoints: { xs: number; s: string; m: string; l: string; xl: string; }; euiBreakpointKeys: string; euiShadowColor: string; euiSize: string; euiSizeXS: string; euiSizeS: string; euiSizeM: string; euiSizeL: string; euiSizeXL: string; euiSizeXXL: string; euiButtonMinWidth: string; euiScrollBar: string; euiScrollBarCorner: string; euiScrollBarCornerThin: string; euiFocusRingColor: string; euiFocusRingAnimStartColor: string; euiFocusRingAnimStartSize: string; euiFocusRingAnimStartSizeLarge: string; euiFocusRingSizeLarge: string; euiFocusRingSize: string; euiFocusTransparency: number; euiFocusTransparencyPercent: string; euiFocusBackgroundColor: string; euiTooltipBackgroundColor: string; euiTooltipBorderColor: string; euiTooltipAnimations: { top: string; left: string; bottom: string; right: string; }; euiFontFamily: string; euiCodeFontFamily: string; euiFontFeatureSettings: string; euiTextScale: string; euiFontSize: string; euiFontSizeXS: string; euiFontSizeS: string; euiFontSizeM: string; euiFontSizeL: string; euiFontSizeXL: string; euiFontSizeXXL: string; euiLineHeight: number; euiBodyLineHeight: number; euiTitles: { xxxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; s: { 'font-size': string; 'line-height': string; 'font-weight': number; }; m: { 'font-size': string; 'line-height': string; 'font-weight': number; }; l: { 'font-size': string; 'line-height': string; 'font-weight': number; }; }; euiZLevel0: number; euiZLevel1: number; euiZLevel2: number; euiZLevel3: number; euiZLevel4: number; euiZLevel5: number; euiZLevel6: number; euiZLevel7: number; euiZLevel8: number; euiZLevel9: number; euiZToastList: number; euiZModal: number; euiZMask: number; euiZNavigation: number; euiZContentMenu: number; euiZHeader: number; euiZFlyout: number; euiZMaskBelowHeader: number; euiZContent: number; euiColorGhost: string; euiColorInk: string; euiColorPrimary: string; euiColorAccent: string; euiColorSuccess: string; euiColorWarning: string; euiColorDanger: string; euiColorEmptyShade: string; euiColorLightestShade: string; euiColorLightShade: string; euiColorMediumShade: string; euiColorDarkShade: string; euiColorDarkestShade: string; euiColorFullShade: string; euiPageBackgroundColor: string; euiColorHighlight: string; euiTextColor: string; euiTitleColor: string; euiTextSubduedColor: string; euiColorDisabled: string; euiColorPrimaryText: string; euiColorSuccessText: string; euiColorAccentText: string; euiColorWarningText: string; euiColorDangerText: string; euiColorDisabledText: string; euiLinkColor: string; euiColorChartLines: string; euiColorChartBand: string; euiDatePickerCalendarWidth: string; euiDatePickerPadding: string; euiDatePickerGap: string; euiDatePickerCalendarColumns: number; euiDatePickerButtonSize: string; euiDatePickerMinControlWidth: string; euiDatePickerMaxControlWidth: string; euiButtonDefaultTransparency: number; euiButtonFontWeight: number; euiStepStatusColors: { default: string; complete: string; warning: string; danger: string; }; }" + "{ euiZDataGrid: number; euiZHeaderBelowDataGrid: number; euiZDataGridCellPopover: number; euiDataGridCellPaddingS: string; euiDataGridCellPaddingM: string; euiDataGridCellPaddingL: string; euiTableHoverColor: string; euiTableSelectedColor: string; euiTableHoverSelectedColor: string; euiTableActionsBorderColor: string; euiTableHoverClickableColor: string; euiTableFocusClickableColor: string; euiContrastRatioText: number; euiContrastRatioGraphic: number; euiContrastRatioDisabled: number; euiAnimSlightBounce: string; euiAnimSlightResistance: string; euiAnimSpeedExtraFast: string; euiAnimSpeedFast: string; euiAnimSpeedNormal: string; euiAnimSpeedSlow: string; euiAnimSpeedExtraSlow: string; euiBorderWidthThin: string; euiBorderWidthThick: string; euiBorderColor: string; euiBorderRadius: string; euiBorderRadiusSmall: string; euiBorderThick: string; euiBorderThin: string; euiBorderEditable: string; euiButtonHeight: string; euiButtonHeightSmall: string; euiButtonHeightXSmall: string; euiButtonColorDisabled: string; euiButtonColorDisabledText: string; euiButtonColorGhostDisabled: string; euiButtonTypes: { primary: string; accent: string; success: string; warning: string; danger: string; ghost: string; text: string; }; euiPaletteColorBlind: { euiColorVis0: { graphic: string; behindText: string; }; euiColorVis1: { graphic: string; behindText: string; }; euiColorVis2: { graphic: string; behindText: string; }; euiColorVis3: { graphic: string; behindText: string; }; euiColorVis4: { graphic: string; behindText: string; }; euiColorVis5: { graphic: string; behindText: string; }; euiColorVis6: { graphic: string; behindText: string; }; euiColorVis7: { graphic: string; behindText: string; }; euiColorVis8: { graphic: string; behindText: string; }; euiColorVis9: { graphic: string; behindText: string; }; }; euiPaletteColorBlindKeys: string; euiColorVis0: string; euiColorVis1: string; euiColorVis2: string; euiColorVis3: string; euiColorVis4: string; euiColorVis5: string; euiColorVis6: string; euiColorVis7: string; euiColorVis8: string; euiColorVis9: string; euiColorVis0_behindText: string; euiColorVis1_behindText: string; euiColorVis2_behindText: string; euiColorVis3_behindText: string; euiColorVis4_behindText: string; euiColorVis5_behindText: string; euiColorVis6_behindText: string; euiColorVis7_behindText: string; euiColorVis8_behindText: string; euiColorVis9_behindText: string; euiFontWeightLight: number; euiFontWeightRegular: number; euiFontWeightMedium: number; euiFontWeightSemiBold: number; euiFontWeightBold: number; euiCodeFontWeightRegular: number; euiCodeFontWeightBold: number; euiFormMaxWidth: string; euiFormControlHeight: string; euiFormControlCompressedHeight: string; euiFormControlPadding: string; euiFormControlCompressedPadding: string; euiFormControlBorderRadius: string; euiFormControlCompressedBorderRadius: string; euiRadioSize: string; euiCheckBoxSize: string; euiCheckboxBorderRadius: string; euiSwitchHeight: string; euiSwitchWidth: string; euiSwitchThumbSize: string; euiSwitchIconHeight: string; euiSwitchHeightCompressed: string; euiSwitchWidthCompressed: string; euiSwitchThumbSizeCompressed: string; euiSwitchHeightMini: string; euiSwitchWidthMini: string; euiSwitchThumbSizeMini: string; euiFormBackgroundColor: string; euiFormBackgroundDisabledColor: string; euiFormBackgroundReadOnlyColor: string; euiFormBorderOpaqueColor: string; euiFormBorderColor: string; euiFormBorderDisabledColor: string; euiFormCustomControlDisabledIconColor: string; euiFormCustomControlBorderColor: string; euiFormControlDisabledColor: string; euiFormControlBoxShadow: string; euiFormControlPlaceholderText: string; euiFormInputGroupLabelBackground: string; euiFormInputGroupBorder: string; euiSwitchOffColor: string; euiFormControlIconSizes: { small: string; medium: string; large: string; xLarge: string; xxLarge: string; }; euiFormControlLayoutGroupInputHeight: string; euiFormControlLayoutGroupInputCompressedHeight: string; euiFormControlLayoutGroupInputCompressedBorderRadius: string; euiHeaderBackgroundColor: string; euiHeaderDarkBackgroundColor: string; euiHeaderBorderColor: string; euiHeaderBreadcrumbColor: string; euiHeaderHeight: string; euiHeaderChildSize: string; euiHeaderHeightCompensation: string; euiPageDefaultMaxWidth: string; euiPageSidebarMinWidth: string; euiPanelPaddingModifiers: { paddingSmall: string; paddingMedium: string; paddingLarge: string; }; euiPanelBorderRadiusModifiers: { borderRadiusNone: number; borderRadiusMedium: string; }; euiPanelBackgroundColorModifiers: { transparent: string; plain: string; subdued: string; accent: string; primary: string; success: string; warning: string; danger: string; }; euiBreakpoints: { xs: number; s: string; m: string; l: string; xl: string; }; euiBreakpointKeys: string; euiShadowColor: string; euiSize: string; euiSizeXS: string; euiSizeS: string; euiSizeM: string; euiSizeL: string; euiSizeXL: string; euiSizeXXL: string; euiScrollBar: string; euiScrollBarCorner: string; euiScrollBarCornerThin: string; euiFocusRingColor: string; euiFocusRingAnimStartColor: string; euiFocusRingAnimStartSize: string; euiFocusRingAnimStartSizeLarge: string; euiFocusRingSizeLarge: string; euiFocusRingSize: string; euiFocusTransparency: number; euiFocusTransparencyPercent: string; euiFocusBackgroundColor: string; euiFontFamily: string; euiCodeFontFamily: string; euiFontFeatureSettings: string; euiTextScale: string; euiFontSize: string; euiFontSizeXS: string; euiFontSizeS: string; euiFontSizeM: string; euiFontSizeL: string; euiFontSizeXL: string; euiFontSizeXXL: string; euiLineHeight: number; euiBodyLineHeight: number; euiTitles: { xxxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; s: { 'font-size': string; 'line-height': string; 'font-weight': number; }; m: { 'font-size': string; 'line-height': string; 'font-weight': number; }; l: { 'font-size': string; 'line-height': string; 'font-weight': number; }; }; euiZLevel0: number; euiZLevel1: number; euiZLevel2: number; euiZLevel3: number; euiZLevel4: number; euiZLevel5: number; euiZLevel6: number; euiZLevel7: number; euiZLevel8: number; euiZLevel9: number; euiZToastList: number; euiZModal: number; euiZMask: number; euiZNavigation: number; euiZContentMenu: number; euiZHeader: number; euiZFlyout: number; euiZMaskBelowHeader: number; euiZContent: number; euiColorGhost: string; euiColorInk: string; euiColorPrimary: string; euiColorAccent: string; euiColorSuccess: string; euiColorWarning: string; euiColorDanger: string; euiColorEmptyShade: string; euiColorLightestShade: string; euiColorLightShade: string; euiColorMediumShade: string; euiColorDarkShade: string; euiColorDarkestShade: string; euiColorFullShade: string; euiPageBackgroundColor: string; euiColorHighlight: string; euiTextColor: string; euiTitleColor: string; euiTextSubduedColor: string; euiColorDisabled: string; euiColorPrimaryText: string; euiColorSuccessText: string; euiColorAccentText: string; euiColorWarningText: string; euiColorDangerText: string; euiColorDisabledText: string; euiLinkColor: string; euiColorChartLines: string; euiColorChartBand: string; }" ], "path": "packages/kbn-ui-theme/src/theme.ts", "deprecated": false, @@ -86,7 +86,7 @@ "label": "euiDarkVars", "description": [], "signature": [ - "{ euiCollapsibleNavGroupLightBackgroundColor: string; euiCollapsibleNavGroupDarkBackgroundColor: string; euiCollapsibleNavGroupDarkHighContrastColor: string; euiColorPickerValueRange0: string; euiColorPickerValueRange1: string; euiColorPickerSaturationRange0: string; euiColorPickerSaturationRange1: string; euiColorPickerIndicatorSize: string; euiColorPickerWidth: string; euiColorPaletteDisplaySizes: { sizeExtraSmall: string; sizeSmall: string; sizeMedium: string; }; euiContextMenuWidth: string; euiControlBarBackground: string; euiControlBarText: string; euiControlBarBorderColor: string; euiControlBarInitialHeight: string; euiControlBarMaxHeight: string; euiControlBarHeights: { s: string; m: string; l: string; }; euiDataGridPrefix: string; euiDataGridStyles: string; euiZDataGrid: number; euiZHeaderBelowDataGrid: number; euiZDataGridCellPopover: number; euiDataGridColumnResizerWidth: string; euiDataGridPopoverMaxHeight: string; euiDataGridCellPaddingS: string; euiDataGridCellPaddingM: string; euiDataGridCellPaddingL: string; euiDataGridVerticalBorder: string; euiSuperDatePickerWidth: string; euiSuperDatePickerButtonWidth: string; euiDragAndDropSpacing: { s: string; m: string; l: string; }; euiEmptyPromptContentMaxWidth: string; euiFilePickerTallHeight: string; euiRangeLevelColors: { primary: string; success: string; warning: string; danger: string; }; textareaResizing: { vertical: string; horizontal: string; both: string; none: string; }; euiHeaderLinksGutterSizes: { gutterXS: string; gutterS: string; gutterM: string; gutterL: string; }; euiKeyPadMenuSize: string; euiKeyPadMenuMarginSize: string; euiMarkdownEditorMinHeight: string; euiResizableButtonTransitionSpeed: string; euiResizableButtonSize: string; euiSelectableListItemBorder: string; euiSelectableListItemPadding: string; euiSelectableTemplateSitewideTypes: { application: { color: string; 'font-weight': number; }; deployment: { color: string; 'font-weight': number; }; article: { color: string; 'font-weight': number; }; case: { color: string; 'font-weight': number; }; platform: { color: string; 'font-weight': number; }; }; euiSideNavEmphasizedBackgroundColor: string; euiSideNavRootTextcolor: string; euiSideNavBranchTextcolor: string; euiSideNavSelectedTextcolor: string; euiSideNavDisabledTextcolor: string; euiStepNumberSize: string; euiStepNumberSmallSize: string; euiStepNumberMargin: string; euiStepStatusColorsToFade: { warning: string; danger: string; disabled: string; incomplete: string; }; euiSuggestItemColors: { tint0: string; tint1: string; tint2: string; tint3: string; tint4: string; tint5: string; tint6: string; tint7: string; tint8: string; tint9: string; tint10: string; }; euiTableCellContentPadding: string; euiTableCellContentPaddingCompressed: string; euiTableCellCheckboxWidth: string; euiTableActionsAreaWidth: string; euiTableHoverColor: string; euiTableSelectedColor: string; euiTableHoverSelectedColor: string; euiTableActionsBorderColor: string; euiTableHoverClickableColor: string; euiTableFocusClickableColor: string; euiContrastRatioText: number; euiContrastRatioGraphic: number; euiContrastRatioDisabled: number; euiAnimSlightBounce: string; euiAnimSlightResistance: string; euiAnimSpeedExtraFast: string; euiAnimSpeedFast: string; euiAnimSpeedNormal: string; euiAnimSpeedSlow: string; euiAnimSpeedExtraSlow: string; euiBorderWidthThin: string; euiBorderWidthThick: string; euiBorderColor: string; euiBorderRadius: string; euiBorderRadiusSmall: string; euiBorderThick: string; euiBorderThin: string; euiBorderEditable: string; euiButtonHeight: string; euiButtonHeightSmall: string; euiButtonHeightXSmall: string; euiButtonColorDisabled: string; euiButtonColorDisabledText: string; euiButtonColorGhostDisabled: string; euiButtonTypes: { primary: string; accent: string; success: string; warning: string; danger: string; ghost: string; text: string; }; euiPaletteColorBlind: { euiColorVis0: { graphic: string; behindText: string; }; euiColorVis1: { graphic: string; behindText: string; }; euiColorVis2: { graphic: string; behindText: string; }; euiColorVis3: { graphic: string; behindText: string; }; euiColorVis4: { graphic: string; behindText: string; }; euiColorVis5: { graphic: string; behindText: string; }; euiColorVis6: { graphic: string; behindText: string; }; euiColorVis7: { graphic: string; behindText: string; }; euiColorVis8: { graphic: string; behindText: string; }; euiColorVis9: { graphic: string; behindText: string; }; }; euiPaletteColorBlindKeys: string; euiColorVis0: string; euiColorVis1: string; euiColorVis2: string; euiColorVis3: string; euiColorVis4: string; euiColorVis5: string; euiColorVis6: string; euiColorVis7: string; euiColorVis8: string; euiColorVis9: string; euiColorVis0_behindText: string; euiColorVis1_behindText: string; euiColorVis2_behindText: string; euiColorVis3_behindText: string; euiColorVis4_behindText: string; euiColorVis5_behindText: string; euiColorVis6_behindText: string; euiColorVis7_behindText: string; euiColorVis8_behindText: string; euiColorVis9_behindText: string; euiFontWeightLight: number; euiFontWeightRegular: number; euiFontWeightMedium: number; euiFontWeightSemiBold: number; euiFontWeightBold: number; euiCodeFontWeightRegular: number; euiCodeFontWeightBold: number; euiFormMaxWidth: string; euiFormControlHeight: string; euiFormControlCompressedHeight: string; euiFormControlPadding: string; euiFormControlCompressedPadding: string; euiFormControlBorderRadius: string; euiFormControlCompressedBorderRadius: string; euiRadioSize: string; euiCheckBoxSize: string; euiCheckboxBorderRadius: string; euiSwitchHeight: string; euiSwitchWidth: string; euiSwitchThumbSize: string; euiSwitchIconHeight: string; euiSwitchHeightCompressed: string; euiSwitchWidthCompressed: string; euiSwitchThumbSizeCompressed: string; euiSwitchHeightMini: string; euiSwitchWidthMini: string; euiSwitchThumbSizeMini: string; euiFormBackgroundColor: string; euiFormBackgroundDisabledColor: string; euiFormBackgroundReadOnlyColor: string; euiFormBorderOpaqueColor: string; euiFormBorderColor: string; euiFormBorderDisabledColor: string; euiFormCustomControlDisabledIconColor: string; euiFormCustomControlBorderColor: string; euiFormControlDisabledColor: string; euiFormControlBoxShadow: string; euiFormControlPlaceholderText: string; euiFormInputGroupLabelBackground: string; euiFormInputGroupBorder: string; euiSwitchOffColor: string; euiFormControlIconSizes: { small: string; medium: string; large: string; xLarge: string; xxLarge: string; }; euiFormControlLayoutGroupInputHeight: string; euiFormControlLayoutGroupInputCompressedHeight: string; euiFormControlLayoutGroupInputCompressedBorderRadius: string; euiRangeTrackColor: string; euiRangeHighlightColor: string; euiRangeThumbRadius: string; euiRangeThumbHeight: string; euiRangeThumbWidth: string; euiRangeThumbBorderColor: string; euiRangeThumbBackgroundColor: string; euiRangeTrackWidth: string; euiRangeTrackHeight: string; euiRangeTrackCompressedHeight: string; euiRangeTrackBorderWidth: number; euiRangeTrackBorderColor: string; euiRangeTrackRadius: string; euiRangeDisabledOpacity: number; euiRangeHighlightHeight: string; euiRangeHighlightCompressedHeight: string; euiRangeHeight: string; euiRangeCompressedHeight: string; euiHeaderBackgroundColor: string; euiHeaderDarkBackgroundColor: string; euiHeaderBorderColor: string; euiHeaderBreadcrumbColor: string; euiHeaderHeight: string; euiHeaderChildSize: string; euiHeaderHeightCompensation: string; euiPageDefaultMaxWidth: string; euiPageSidebarMinWidth: string; euiPanelPaddingModifiers: { paddingSmall: string; paddingMedium: string; paddingLarge: string; }; euiPanelBorderRadiusModifiers: { borderRadiusNone: number; borderRadiusMedium: string; }; euiPanelBackgroundColorModifiers: { transparent: string; plain: string; subdued: string; accent: string; primary: string; success: string; warning: string; danger: string; }; euiBreakpoints: { xs: number; s: string; m: string; l: string; xl: string; }; euiBreakpointKeys: string; euiShadowColor: string; euiSize: string; euiSizeXS: string; euiSizeS: string; euiSizeM: string; euiSizeL: string; euiSizeXL: string; euiSizeXXL: string; euiButtonMinWidth: string; euiScrollBar: string; euiScrollBarCorner: string; euiScrollBarCornerThin: string; euiFocusRingColor: string; euiFocusRingAnimStartColor: string; euiFocusRingAnimStartSize: string; euiFocusRingAnimStartSizeLarge: string; euiFocusRingSizeLarge: string; euiFocusRingSize: string; euiFocusTransparency: number; euiFocusTransparencyPercent: string; euiFocusBackgroundColor: string; euiTooltipBackgroundColor: string; euiTooltipBorderColor: string; euiTooltipAnimations: { top: string; left: string; bottom: string; right: string; }; euiFontFamily: string; euiCodeFontFamily: string; euiFontFeatureSettings: string; euiTextScale: string; euiFontSize: string; euiFontSizeXS: string; euiFontSizeS: string; euiFontSizeM: string; euiFontSizeL: string; euiFontSizeXL: string; euiFontSizeXXL: string; euiLineHeight: number; euiBodyLineHeight: number; euiTitles: { xxxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; s: { 'font-size': string; 'line-height': string; 'font-weight': number; }; m: { 'font-size': string; 'line-height': string; 'font-weight': number; }; l: { 'font-size': string; 'line-height': string; 'font-weight': number; }; }; euiZLevel0: number; euiZLevel1: number; euiZLevel2: number; euiZLevel3: number; euiZLevel4: number; euiZLevel5: number; euiZLevel6: number; euiZLevel7: number; euiZLevel8: number; euiZLevel9: number; euiZToastList: number; euiZModal: number; euiZMask: number; euiZNavigation: number; euiZContentMenu: number; euiZHeader: number; euiZFlyout: number; euiZMaskBelowHeader: number; euiZContent: number; euiColorGhost: string; euiColorInk: string; euiColorPrimary: string; euiColorAccent: string; euiColorSuccess: string; euiColorWarning: string; euiColorDanger: string; euiColorEmptyShade: string; euiColorLightestShade: string; euiColorLightShade: string; euiColorMediumShade: string; euiColorDarkShade: string; euiColorDarkestShade: string; euiColorFullShade: string; euiPageBackgroundColor: string; euiColorHighlight: string; euiTextColor: string; euiTitleColor: string; euiTextSubduedColor: string; euiColorDisabled: string; euiColorPrimaryText: string; euiColorSuccessText: string; euiColorAccentText: string; euiColorWarningText: string; euiColorDangerText: string; euiColorDisabledText: string; euiLinkColor: string; euiColorChartLines: string; euiColorChartBand: string; euiDatePickerCalendarWidth: string; euiDatePickerPadding: string; euiDatePickerGap: string; euiDatePickerCalendarColumns: number; euiDatePickerButtonSize: string; euiDatePickerMinControlWidth: string; euiDatePickerMaxControlWidth: string; euiButtonDefaultTransparency: number; euiButtonFontWeight: number; euiStepStatusColors: { default: string; complete: string; warning: string; danger: string; }; }" + "{ euiZDataGrid: number; euiZHeaderBelowDataGrid: number; euiZDataGridCellPopover: number; euiDataGridCellPaddingS: string; euiDataGridCellPaddingM: string; euiDataGridCellPaddingL: string; euiTableHoverColor: string; euiTableSelectedColor: string; euiTableHoverSelectedColor: string; euiTableActionsBorderColor: string; euiTableHoverClickableColor: string; euiTableFocusClickableColor: string; euiContrastRatioText: number; euiContrastRatioGraphic: number; euiContrastRatioDisabled: number; euiAnimSlightBounce: string; euiAnimSlightResistance: string; euiAnimSpeedExtraFast: string; euiAnimSpeedFast: string; euiAnimSpeedNormal: string; euiAnimSpeedSlow: string; euiAnimSpeedExtraSlow: string; euiBorderWidthThin: string; euiBorderWidthThick: string; euiBorderColor: string; euiBorderRadius: string; euiBorderRadiusSmall: string; euiBorderThick: string; euiBorderThin: string; euiBorderEditable: string; euiButtonHeight: string; euiButtonHeightSmall: string; euiButtonHeightXSmall: string; euiButtonColorDisabled: string; euiButtonColorDisabledText: string; euiButtonColorGhostDisabled: string; euiButtonTypes: { primary: string; accent: string; success: string; warning: string; danger: string; ghost: string; text: string; }; euiPaletteColorBlind: { euiColorVis0: { graphic: string; behindText: string; }; euiColorVis1: { graphic: string; behindText: string; }; euiColorVis2: { graphic: string; behindText: string; }; euiColorVis3: { graphic: string; behindText: string; }; euiColorVis4: { graphic: string; behindText: string; }; euiColorVis5: { graphic: string; behindText: string; }; euiColorVis6: { graphic: string; behindText: string; }; euiColorVis7: { graphic: string; behindText: string; }; euiColorVis8: { graphic: string; behindText: string; }; euiColorVis9: { graphic: string; behindText: string; }; }; euiPaletteColorBlindKeys: string; euiColorVis0: string; euiColorVis1: string; euiColorVis2: string; euiColorVis3: string; euiColorVis4: string; euiColorVis5: string; euiColorVis6: string; euiColorVis7: string; euiColorVis8: string; euiColorVis9: string; euiColorVis0_behindText: string; euiColorVis1_behindText: string; euiColorVis2_behindText: string; euiColorVis3_behindText: string; euiColorVis4_behindText: string; euiColorVis5_behindText: string; euiColorVis6_behindText: string; euiColorVis7_behindText: string; euiColorVis8_behindText: string; euiColorVis9_behindText: string; euiFontWeightLight: number; euiFontWeightRegular: number; euiFontWeightMedium: number; euiFontWeightSemiBold: number; euiFontWeightBold: number; euiCodeFontWeightRegular: number; euiCodeFontWeightBold: number; euiFormMaxWidth: string; euiFormControlHeight: string; euiFormControlCompressedHeight: string; euiFormControlPadding: string; euiFormControlCompressedPadding: string; euiFormControlBorderRadius: string; euiFormControlCompressedBorderRadius: string; euiRadioSize: string; euiCheckBoxSize: string; euiCheckboxBorderRadius: string; euiSwitchHeight: string; euiSwitchWidth: string; euiSwitchThumbSize: string; euiSwitchIconHeight: string; euiSwitchHeightCompressed: string; euiSwitchWidthCompressed: string; euiSwitchThumbSizeCompressed: string; euiSwitchHeightMini: string; euiSwitchWidthMini: string; euiSwitchThumbSizeMini: string; euiFormBackgroundColor: string; euiFormBackgroundDisabledColor: string; euiFormBackgroundReadOnlyColor: string; euiFormBorderOpaqueColor: string; euiFormBorderColor: string; euiFormBorderDisabledColor: string; euiFormCustomControlDisabledIconColor: string; euiFormCustomControlBorderColor: string; euiFormControlDisabledColor: string; euiFormControlBoxShadow: string; euiFormControlPlaceholderText: string; euiFormInputGroupLabelBackground: string; euiFormInputGroupBorder: string; euiSwitchOffColor: string; euiFormControlIconSizes: { small: string; medium: string; large: string; xLarge: string; xxLarge: string; }; euiFormControlLayoutGroupInputHeight: string; euiFormControlLayoutGroupInputCompressedHeight: string; euiFormControlLayoutGroupInputCompressedBorderRadius: string; euiHeaderBackgroundColor: string; euiHeaderDarkBackgroundColor: string; euiHeaderBorderColor: string; euiHeaderBreadcrumbColor: string; euiHeaderHeight: string; euiHeaderChildSize: string; euiHeaderHeightCompensation: string; euiPageDefaultMaxWidth: string; euiPageSidebarMinWidth: string; euiPanelPaddingModifiers: { paddingSmall: string; paddingMedium: string; paddingLarge: string; }; euiPanelBorderRadiusModifiers: { borderRadiusNone: number; borderRadiusMedium: string; }; euiPanelBackgroundColorModifiers: { transparent: string; plain: string; subdued: string; accent: string; primary: string; success: string; warning: string; danger: string; }; euiBreakpoints: { xs: number; s: string; m: string; l: string; xl: string; }; euiBreakpointKeys: string; euiShadowColor: string; euiSize: string; euiSizeXS: string; euiSizeS: string; euiSizeM: string; euiSizeL: string; euiSizeXL: string; euiSizeXXL: string; euiScrollBar: string; euiScrollBarCorner: string; euiScrollBarCornerThin: string; euiFocusRingColor: string; euiFocusRingAnimStartColor: string; euiFocusRingAnimStartSize: string; euiFocusRingAnimStartSizeLarge: string; euiFocusRingSizeLarge: string; euiFocusRingSize: string; euiFocusTransparency: number; euiFocusTransparencyPercent: string; euiFocusBackgroundColor: string; euiFontFamily: string; euiCodeFontFamily: string; euiFontFeatureSettings: string; euiTextScale: string; euiFontSize: string; euiFontSizeXS: string; euiFontSizeS: string; euiFontSizeM: string; euiFontSizeL: string; euiFontSizeXL: string; euiFontSizeXXL: string; euiLineHeight: number; euiBodyLineHeight: number; euiTitles: { xxxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; s: { 'font-size': string; 'line-height': string; 'font-weight': number; }; m: { 'font-size': string; 'line-height': string; 'font-weight': number; }; l: { 'font-size': string; 'line-height': string; 'font-weight': number; }; }; euiZLevel0: number; euiZLevel1: number; euiZLevel2: number; euiZLevel3: number; euiZLevel4: number; euiZLevel5: number; euiZLevel6: number; euiZLevel7: number; euiZLevel8: number; euiZLevel9: number; euiZToastList: number; euiZModal: number; euiZMask: number; euiZNavigation: number; euiZContentMenu: number; euiZHeader: number; euiZFlyout: number; euiZMaskBelowHeader: number; euiZContent: number; euiColorGhost: string; euiColorInk: string; euiColorPrimary: string; euiColorAccent: string; euiColorSuccess: string; euiColorWarning: string; euiColorDanger: string; euiColorEmptyShade: string; euiColorLightestShade: string; euiColorLightShade: string; euiColorMediumShade: string; euiColorDarkShade: string; euiColorDarkestShade: string; euiColorFullShade: string; euiPageBackgroundColor: string; euiColorHighlight: string; euiTextColor: string; euiTitleColor: string; euiTextSubduedColor: string; euiColorDisabled: string; euiColorPrimaryText: string; euiColorSuccessText: string; euiColorAccentText: string; euiColorWarningText: string; euiColorDangerText: string; euiColorDisabledText: string; euiLinkColor: string; euiColorChartLines: string; euiColorChartBand: string; }" ], "path": "packages/kbn-ui-theme/src/theme.ts", "deprecated": false, @@ -101,7 +101,7 @@ "label": "euiLightVars", "description": [], "signature": [ - "{ euiCollapsibleNavGroupLightBackgroundColor: string; euiCollapsibleNavGroupDarkBackgroundColor: string; euiCollapsibleNavGroupDarkHighContrastColor: string; euiColorPickerValueRange0: string; euiColorPickerValueRange1: string; euiColorPickerSaturationRange0: string; euiColorPickerSaturationRange1: string; euiColorPickerIndicatorSize: string; euiColorPickerWidth: string; euiColorPaletteDisplaySizes: { sizeExtraSmall: string; sizeSmall: string; sizeMedium: string; }; euiContextMenuWidth: string; euiControlBarBackground: string; euiControlBarText: string; euiControlBarBorderColor: string; euiControlBarInitialHeight: string; euiControlBarMaxHeight: string; euiControlBarHeights: { s: string; m: string; l: string; }; euiDataGridPrefix: string; euiDataGridStyles: string; euiZDataGrid: number; euiZHeaderBelowDataGrid: number; euiZDataGridCellPopover: number; euiDataGridColumnResizerWidth: string; euiDataGridPopoverMaxHeight: string; euiDataGridCellPaddingS: string; euiDataGridCellPaddingM: string; euiDataGridCellPaddingL: string; euiDataGridVerticalBorder: string; euiSuperDatePickerWidth: string; euiSuperDatePickerButtonWidth: string; euiDragAndDropSpacing: { s: string; m: string; l: string; }; euiEmptyPromptContentMaxWidth: string; euiFilePickerTallHeight: string; euiRangeLevelColors: { primary: string; success: string; warning: string; danger: string; }; textareaResizing: { vertical: string; horizontal: string; both: string; none: string; }; euiHeaderLinksGutterSizes: { gutterXS: string; gutterS: string; gutterM: string; gutterL: string; }; euiKeyPadMenuSize: string; euiKeyPadMenuMarginSize: string; euiMarkdownEditorMinHeight: string; euiResizableButtonTransitionSpeed: string; euiResizableButtonSize: string; euiSelectableListItemBorder: string; euiSelectableListItemPadding: string; euiSelectableTemplateSitewideTypes: { application: { color: string; 'font-weight': number; }; deployment: { color: string; 'font-weight': number; }; article: { color: string; 'font-weight': number; }; case: { color: string; 'font-weight': number; }; platform: { color: string; 'font-weight': number; }; }; euiSideNavEmphasizedBackgroundColor: string; euiSideNavRootTextcolor: string; euiSideNavBranchTextcolor: string; euiSideNavSelectedTextcolor: string; euiSideNavDisabledTextcolor: string; euiStepNumberSize: string; euiStepNumberSmallSize: string; euiStepNumberMargin: string; euiStepStatusColorsToFade: { warning: string; danger: string; disabled: string; incomplete: string; }; euiSuggestItemColors: { tint0: string; tint1: string; tint2: string; tint3: string; tint4: string; tint5: string; tint6: string; tint7: string; tint8: string; tint9: string; tint10: string; }; euiTableCellContentPadding: string; euiTableCellContentPaddingCompressed: string; euiTableCellCheckboxWidth: string; euiTableActionsAreaWidth: string; euiTableHoverColor: string; euiTableSelectedColor: string; euiTableHoverSelectedColor: string; euiTableActionsBorderColor: string; euiTableHoverClickableColor: string; euiTableFocusClickableColor: string; euiContrastRatioText: number; euiContrastRatioGraphic: number; euiContrastRatioDisabled: number; euiAnimSlightBounce: string; euiAnimSlightResistance: string; euiAnimSpeedExtraFast: string; euiAnimSpeedFast: string; euiAnimSpeedNormal: string; euiAnimSpeedSlow: string; euiAnimSpeedExtraSlow: string; euiBorderWidthThin: string; euiBorderWidthThick: string; euiBorderColor: string; euiBorderRadius: string; euiBorderRadiusSmall: string; euiBorderThick: string; euiBorderThin: string; euiBorderEditable: string; euiButtonHeight: string; euiButtonHeightSmall: string; euiButtonHeightXSmall: string; euiButtonColorDisabled: string; euiButtonColorDisabledText: string; euiButtonColorGhostDisabled: string; euiButtonTypes: { primary: string; accent: string; success: string; warning: string; danger: string; ghost: string; text: string; }; euiPaletteColorBlind: { euiColorVis0: { graphic: string; behindText: string; }; euiColorVis1: { graphic: string; behindText: string; }; euiColorVis2: { graphic: string; behindText: string; }; euiColorVis3: { graphic: string; behindText: string; }; euiColorVis4: { graphic: string; behindText: string; }; euiColorVis5: { graphic: string; behindText: string; }; euiColorVis6: { graphic: string; behindText: string; }; euiColorVis7: { graphic: string; behindText: string; }; euiColorVis8: { graphic: string; behindText: string; }; euiColorVis9: { graphic: string; behindText: string; }; }; euiPaletteColorBlindKeys: string; euiColorVis0: string; euiColorVis1: string; euiColorVis2: string; euiColorVis3: string; euiColorVis4: string; euiColorVis5: string; euiColorVis6: string; euiColorVis7: string; euiColorVis8: string; euiColorVis9: string; euiColorVis0_behindText: string; euiColorVis1_behindText: string; euiColorVis2_behindText: string; euiColorVis3_behindText: string; euiColorVis4_behindText: string; euiColorVis5_behindText: string; euiColorVis6_behindText: string; euiColorVis7_behindText: string; euiColorVis8_behindText: string; euiColorVis9_behindText: string; euiFontWeightLight: number; euiFontWeightRegular: number; euiFontWeightMedium: number; euiFontWeightSemiBold: number; euiFontWeightBold: number; euiCodeFontWeightRegular: number; euiCodeFontWeightBold: number; euiFormMaxWidth: string; euiFormControlHeight: string; euiFormControlCompressedHeight: string; euiFormControlPadding: string; euiFormControlCompressedPadding: string; euiFormControlBorderRadius: string; euiFormControlCompressedBorderRadius: string; euiRadioSize: string; euiCheckBoxSize: string; euiCheckboxBorderRadius: string; euiSwitchHeight: string; euiSwitchWidth: string; euiSwitchThumbSize: string; euiSwitchIconHeight: string; euiSwitchHeightCompressed: string; euiSwitchWidthCompressed: string; euiSwitchThumbSizeCompressed: string; euiSwitchHeightMini: string; euiSwitchWidthMini: string; euiSwitchThumbSizeMini: string; euiFormBackgroundColor: string; euiFormBackgroundDisabledColor: string; euiFormBackgroundReadOnlyColor: string; euiFormBorderOpaqueColor: string; euiFormBorderColor: string; euiFormBorderDisabledColor: string; euiFormCustomControlDisabledIconColor: string; euiFormCustomControlBorderColor: string; euiFormControlDisabledColor: string; euiFormControlBoxShadow: string; euiFormControlPlaceholderText: string; euiFormInputGroupLabelBackground: string; euiFormInputGroupBorder: string; euiSwitchOffColor: string; euiFormControlIconSizes: { small: string; medium: string; large: string; xLarge: string; xxLarge: string; }; euiFormControlLayoutGroupInputHeight: string; euiFormControlLayoutGroupInputCompressedHeight: string; euiFormControlLayoutGroupInputCompressedBorderRadius: string; euiRangeTrackColor: string; euiRangeHighlightColor: string; euiRangeThumbRadius: string; euiRangeThumbHeight: string; euiRangeThumbWidth: string; euiRangeThumbBorderColor: string; euiRangeThumbBackgroundColor: string; euiRangeTrackWidth: string; euiRangeTrackHeight: string; euiRangeTrackCompressedHeight: string; euiRangeTrackBorderWidth: number; euiRangeTrackBorderColor: string; euiRangeTrackRadius: string; euiRangeDisabledOpacity: number; euiRangeHighlightHeight: string; euiRangeHighlightCompressedHeight: string; euiRangeHeight: string; euiRangeCompressedHeight: string; euiHeaderBackgroundColor: string; euiHeaderDarkBackgroundColor: string; euiHeaderBorderColor: string; euiHeaderBreadcrumbColor: string; euiHeaderHeight: string; euiHeaderChildSize: string; euiHeaderHeightCompensation: string; euiPageDefaultMaxWidth: string; euiPageSidebarMinWidth: string; euiPanelPaddingModifiers: { paddingSmall: string; paddingMedium: string; paddingLarge: string; }; euiPanelBorderRadiusModifiers: { borderRadiusNone: number; borderRadiusMedium: string; }; euiPanelBackgroundColorModifiers: { transparent: string; plain: string; subdued: string; accent: string; primary: string; success: string; warning: string; danger: string; }; euiBreakpoints: { xs: number; s: string; m: string; l: string; xl: string; }; euiBreakpointKeys: string; euiShadowColor: string; euiSize: string; euiSizeXS: string; euiSizeS: string; euiSizeM: string; euiSizeL: string; euiSizeXL: string; euiSizeXXL: string; euiButtonMinWidth: string; euiScrollBar: string; euiScrollBarCorner: string; euiScrollBarCornerThin: string; euiFocusRingColor: string; euiFocusRingAnimStartColor: string; euiFocusRingAnimStartSize: string; euiFocusRingAnimStartSizeLarge: string; euiFocusRingSizeLarge: string; euiFocusRingSize: string; euiFocusTransparency: number; euiFocusTransparencyPercent: string; euiFocusBackgroundColor: string; euiTooltipBackgroundColor: string; euiTooltipBorderColor: string; euiTooltipAnimations: { top: string; left: string; bottom: string; right: string; }; euiFontFamily: string; euiCodeFontFamily: string; euiFontFeatureSettings: string; euiTextScale: string; euiFontSize: string; euiFontSizeXS: string; euiFontSizeS: string; euiFontSizeM: string; euiFontSizeL: string; euiFontSizeXL: string; euiFontSizeXXL: string; euiLineHeight: number; euiBodyLineHeight: number; euiTitles: { xxxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; s: { 'font-size': string; 'line-height': string; 'font-weight': number; }; m: { 'font-size': string; 'line-height': string; 'font-weight': number; }; l: { 'font-size': string; 'line-height': string; 'font-weight': number; }; }; euiZLevel0: number; euiZLevel1: number; euiZLevel2: number; euiZLevel3: number; euiZLevel4: number; euiZLevel5: number; euiZLevel6: number; euiZLevel7: number; euiZLevel8: number; euiZLevel9: number; euiZToastList: number; euiZModal: number; euiZMask: number; euiZNavigation: number; euiZContentMenu: number; euiZHeader: number; euiZFlyout: number; euiZMaskBelowHeader: number; euiZContent: number; euiColorGhost: string; euiColorInk: string; euiColorPrimary: string; euiColorAccent: string; euiColorSuccess: string; euiColorWarning: string; euiColorDanger: string; euiColorEmptyShade: string; euiColorLightestShade: string; euiColorLightShade: string; euiColorMediumShade: string; euiColorDarkShade: string; euiColorDarkestShade: string; euiColorFullShade: string; euiPageBackgroundColor: string; euiColorHighlight: string; euiTextColor: string; euiTitleColor: string; euiTextSubduedColor: string; euiColorDisabled: string; euiColorPrimaryText: string; euiColorSuccessText: string; euiColorAccentText: string; euiColorWarningText: string; euiColorDangerText: string; euiColorDisabledText: string; euiLinkColor: string; euiColorChartLines: string; euiColorChartBand: string; euiDatePickerCalendarWidth: string; euiDatePickerPadding: string; euiDatePickerGap: string; euiDatePickerCalendarColumns: number; euiDatePickerButtonSize: string; euiDatePickerMinControlWidth: string; euiDatePickerMaxControlWidth: string; euiButtonDefaultTransparency: number; euiButtonFontWeight: number; euiStepStatusColors: { default: string; complete: string; warning: string; danger: string; }; }" + "{ euiZDataGrid: number; euiZHeaderBelowDataGrid: number; euiZDataGridCellPopover: number; euiDataGridCellPaddingS: string; euiDataGridCellPaddingM: string; euiDataGridCellPaddingL: string; euiTableHoverColor: string; euiTableSelectedColor: string; euiTableHoverSelectedColor: string; euiTableActionsBorderColor: string; euiTableHoverClickableColor: string; euiTableFocusClickableColor: string; euiContrastRatioText: number; euiContrastRatioGraphic: number; euiContrastRatioDisabled: number; euiAnimSlightBounce: string; euiAnimSlightResistance: string; euiAnimSpeedExtraFast: string; euiAnimSpeedFast: string; euiAnimSpeedNormal: string; euiAnimSpeedSlow: string; euiAnimSpeedExtraSlow: string; euiBorderWidthThin: string; euiBorderWidthThick: string; euiBorderColor: string; euiBorderRadius: string; euiBorderRadiusSmall: string; euiBorderThick: string; euiBorderThin: string; euiBorderEditable: string; euiButtonHeight: string; euiButtonHeightSmall: string; euiButtonHeightXSmall: string; euiButtonColorDisabled: string; euiButtonColorDisabledText: string; euiButtonColorGhostDisabled: string; euiButtonTypes: { primary: string; accent: string; success: string; warning: string; danger: string; ghost: string; text: string; }; euiPaletteColorBlind: { euiColorVis0: { graphic: string; behindText: string; }; euiColorVis1: { graphic: string; behindText: string; }; euiColorVis2: { graphic: string; behindText: string; }; euiColorVis3: { graphic: string; behindText: string; }; euiColorVis4: { graphic: string; behindText: string; }; euiColorVis5: { graphic: string; behindText: string; }; euiColorVis6: { graphic: string; behindText: string; }; euiColorVis7: { graphic: string; behindText: string; }; euiColorVis8: { graphic: string; behindText: string; }; euiColorVis9: { graphic: string; behindText: string; }; }; euiPaletteColorBlindKeys: string; euiColorVis0: string; euiColorVis1: string; euiColorVis2: string; euiColorVis3: string; euiColorVis4: string; euiColorVis5: string; euiColorVis6: string; euiColorVis7: string; euiColorVis8: string; euiColorVis9: string; euiColorVis0_behindText: string; euiColorVis1_behindText: string; euiColorVis2_behindText: string; euiColorVis3_behindText: string; euiColorVis4_behindText: string; euiColorVis5_behindText: string; euiColorVis6_behindText: string; euiColorVis7_behindText: string; euiColorVis8_behindText: string; euiColorVis9_behindText: string; euiFontWeightLight: number; euiFontWeightRegular: number; euiFontWeightMedium: number; euiFontWeightSemiBold: number; euiFontWeightBold: number; euiCodeFontWeightRegular: number; euiCodeFontWeightBold: number; euiFormMaxWidth: string; euiFormControlHeight: string; euiFormControlCompressedHeight: string; euiFormControlPadding: string; euiFormControlCompressedPadding: string; euiFormControlBorderRadius: string; euiFormControlCompressedBorderRadius: string; euiRadioSize: string; euiCheckBoxSize: string; euiCheckboxBorderRadius: string; euiSwitchHeight: string; euiSwitchWidth: string; euiSwitchThumbSize: string; euiSwitchIconHeight: string; euiSwitchHeightCompressed: string; euiSwitchWidthCompressed: string; euiSwitchThumbSizeCompressed: string; euiSwitchHeightMini: string; euiSwitchWidthMini: string; euiSwitchThumbSizeMini: string; euiFormBackgroundColor: string; euiFormBackgroundDisabledColor: string; euiFormBackgroundReadOnlyColor: string; euiFormBorderOpaqueColor: string; euiFormBorderColor: string; euiFormBorderDisabledColor: string; euiFormCustomControlDisabledIconColor: string; euiFormCustomControlBorderColor: string; euiFormControlDisabledColor: string; euiFormControlBoxShadow: string; euiFormControlPlaceholderText: string; euiFormInputGroupLabelBackground: string; euiFormInputGroupBorder: string; euiSwitchOffColor: string; euiFormControlIconSizes: { small: string; medium: string; large: string; xLarge: string; xxLarge: string; }; euiFormControlLayoutGroupInputHeight: string; euiFormControlLayoutGroupInputCompressedHeight: string; euiFormControlLayoutGroupInputCompressedBorderRadius: string; euiHeaderBackgroundColor: string; euiHeaderDarkBackgroundColor: string; euiHeaderBorderColor: string; euiHeaderBreadcrumbColor: string; euiHeaderHeight: string; euiHeaderChildSize: string; euiHeaderHeightCompensation: string; euiPageDefaultMaxWidth: string; euiPageSidebarMinWidth: string; euiPanelPaddingModifiers: { paddingSmall: string; paddingMedium: string; paddingLarge: string; }; euiPanelBorderRadiusModifiers: { borderRadiusNone: number; borderRadiusMedium: string; }; euiPanelBackgroundColorModifiers: { transparent: string; plain: string; subdued: string; accent: string; primary: string; success: string; warning: string; danger: string; }; euiBreakpoints: { xs: number; s: string; m: string; l: string; xl: string; }; euiBreakpointKeys: string; euiShadowColor: string; euiSize: string; euiSizeXS: string; euiSizeS: string; euiSizeM: string; euiSizeL: string; euiSizeXL: string; euiSizeXXL: string; euiScrollBar: string; euiScrollBarCorner: string; euiScrollBarCornerThin: string; euiFocusRingColor: string; euiFocusRingAnimStartColor: string; euiFocusRingAnimStartSize: string; euiFocusRingAnimStartSizeLarge: string; euiFocusRingSizeLarge: string; euiFocusRingSize: string; euiFocusTransparency: number; euiFocusTransparencyPercent: string; euiFocusBackgroundColor: string; euiFontFamily: string; euiCodeFontFamily: string; euiFontFeatureSettings: string; euiTextScale: string; euiFontSize: string; euiFontSizeXS: string; euiFontSizeS: string; euiFontSizeM: string; euiFontSizeL: string; euiFontSizeXL: string; euiFontSizeXXL: string; euiLineHeight: number; euiBodyLineHeight: number; euiTitles: { xxxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; s: { 'font-size': string; 'line-height': string; 'font-weight': number; }; m: { 'font-size': string; 'line-height': string; 'font-weight': number; }; l: { 'font-size': string; 'line-height': string; 'font-weight': number; }; }; euiZLevel0: number; euiZLevel1: number; euiZLevel2: number; euiZLevel3: number; euiZLevel4: number; euiZLevel5: number; euiZLevel6: number; euiZLevel7: number; euiZLevel8: number; euiZLevel9: number; euiZToastList: number; euiZModal: number; euiZMask: number; euiZNavigation: number; euiZContentMenu: number; euiZHeader: number; euiZFlyout: number; euiZMaskBelowHeader: number; euiZContent: number; euiColorGhost: string; euiColorInk: string; euiColorPrimary: string; euiColorAccent: string; euiColorSuccess: string; euiColorWarning: string; euiColorDanger: string; euiColorEmptyShade: string; euiColorLightestShade: string; euiColorLightShade: string; euiColorMediumShade: string; euiColorDarkShade: string; euiColorDarkestShade: string; euiColorFullShade: string; euiPageBackgroundColor: string; euiColorHighlight: string; euiTextColor: string; euiTitleColor: string; euiTextSubduedColor: string; euiColorDisabled: string; euiColorPrimaryText: string; euiColorSuccessText: string; euiColorAccentText: string; euiColorWarningText: string; euiColorDangerText: string; euiColorDisabledText: string; euiLinkColor: string; euiColorChartLines: string; euiColorChartBand: string; }" ], "path": "packages/kbn-ui-theme/src/theme.ts", "deprecated": false, @@ -118,7 +118,7 @@ "\nEUI Theme vars that automatically adjust to light/dark theme" ], "signature": [ - "{ euiCollapsibleNavGroupLightBackgroundColor: string; euiCollapsibleNavGroupDarkBackgroundColor: string; euiCollapsibleNavGroupDarkHighContrastColor: string; euiColorPickerValueRange0: string; euiColorPickerValueRange1: string; euiColorPickerSaturationRange0: string; euiColorPickerSaturationRange1: string; euiColorPickerIndicatorSize: string; euiColorPickerWidth: string; euiColorPaletteDisplaySizes: { sizeExtraSmall: string; sizeSmall: string; sizeMedium: string; }; euiContextMenuWidth: string; euiControlBarBackground: string; euiControlBarText: string; euiControlBarBorderColor: string; euiControlBarInitialHeight: string; euiControlBarMaxHeight: string; euiControlBarHeights: { s: string; m: string; l: string; }; euiDataGridPrefix: string; euiDataGridStyles: string; euiZDataGrid: number; euiZHeaderBelowDataGrid: number; euiZDataGridCellPopover: number; euiDataGridColumnResizerWidth: string; euiDataGridPopoverMaxHeight: string; euiDataGridCellPaddingS: string; euiDataGridCellPaddingM: string; euiDataGridCellPaddingL: string; euiDataGridVerticalBorder: string; euiSuperDatePickerWidth: string; euiSuperDatePickerButtonWidth: string; euiDragAndDropSpacing: { s: string; m: string; l: string; }; euiEmptyPromptContentMaxWidth: string; euiFilePickerTallHeight: string; euiRangeLevelColors: { primary: string; success: string; warning: string; danger: string; }; textareaResizing: { vertical: string; horizontal: string; both: string; none: string; }; euiHeaderLinksGutterSizes: { gutterXS: string; gutterS: string; gutterM: string; gutterL: string; }; euiKeyPadMenuSize: string; euiKeyPadMenuMarginSize: string; euiMarkdownEditorMinHeight: string; euiResizableButtonTransitionSpeed: string; euiResizableButtonSize: string; euiSelectableListItemBorder: string; euiSelectableListItemPadding: string; euiSelectableTemplateSitewideTypes: { application: { color: string; 'font-weight': number; }; deployment: { color: string; 'font-weight': number; }; article: { color: string; 'font-weight': number; }; case: { color: string; 'font-weight': number; }; platform: { color: string; 'font-weight': number; }; }; euiSideNavEmphasizedBackgroundColor: string; euiSideNavRootTextcolor: string; euiSideNavBranchTextcolor: string; euiSideNavSelectedTextcolor: string; euiSideNavDisabledTextcolor: string; euiStepNumberSize: string; euiStepNumberSmallSize: string; euiStepNumberMargin: string; euiStepStatusColorsToFade: { warning: string; danger: string; disabled: string; incomplete: string; }; euiSuggestItemColors: { tint0: string; tint1: string; tint2: string; tint3: string; tint4: string; tint5: string; tint6: string; tint7: string; tint8: string; tint9: string; tint10: string; }; euiTableCellContentPadding: string; euiTableCellContentPaddingCompressed: string; euiTableCellCheckboxWidth: string; euiTableActionsAreaWidth: string; euiTableHoverColor: string; euiTableSelectedColor: string; euiTableHoverSelectedColor: string; euiTableActionsBorderColor: string; euiTableHoverClickableColor: string; euiTableFocusClickableColor: string; euiContrastRatioText: number; euiContrastRatioGraphic: number; euiContrastRatioDisabled: number; euiAnimSlightBounce: string; euiAnimSlightResistance: string; euiAnimSpeedExtraFast: string; euiAnimSpeedFast: string; euiAnimSpeedNormal: string; euiAnimSpeedSlow: string; euiAnimSpeedExtraSlow: string; euiBorderWidthThin: string; euiBorderWidthThick: string; euiBorderColor: string; euiBorderRadius: string; euiBorderRadiusSmall: string; euiBorderThick: string; euiBorderThin: string; euiBorderEditable: string; euiButtonHeight: string; euiButtonHeightSmall: string; euiButtonHeightXSmall: string; euiButtonColorDisabled: string; euiButtonColorDisabledText: string; euiButtonColorGhostDisabled: string; euiButtonTypes: { primary: string; accent: string; success: string; warning: string; danger: string; ghost: string; text: string; }; euiPaletteColorBlind: { euiColorVis0: { graphic: string; behindText: string; }; euiColorVis1: { graphic: string; behindText: string; }; euiColorVis2: { graphic: string; behindText: string; }; euiColorVis3: { graphic: string; behindText: string; }; euiColorVis4: { graphic: string; behindText: string; }; euiColorVis5: { graphic: string; behindText: string; }; euiColorVis6: { graphic: string; behindText: string; }; euiColorVis7: { graphic: string; behindText: string; }; euiColorVis8: { graphic: string; behindText: string; }; euiColorVis9: { graphic: string; behindText: string; }; }; euiPaletteColorBlindKeys: string; euiColorVis0: string; euiColorVis1: string; euiColorVis2: string; euiColorVis3: string; euiColorVis4: string; euiColorVis5: string; euiColorVis6: string; euiColorVis7: string; euiColorVis8: string; euiColorVis9: string; euiColorVis0_behindText: string; euiColorVis1_behindText: string; euiColorVis2_behindText: string; euiColorVis3_behindText: string; euiColorVis4_behindText: string; euiColorVis5_behindText: string; euiColorVis6_behindText: string; euiColorVis7_behindText: string; euiColorVis8_behindText: string; euiColorVis9_behindText: string; euiFontWeightLight: number; euiFontWeightRegular: number; euiFontWeightMedium: number; euiFontWeightSemiBold: number; euiFontWeightBold: number; euiCodeFontWeightRegular: number; euiCodeFontWeightBold: number; euiFormMaxWidth: string; euiFormControlHeight: string; euiFormControlCompressedHeight: string; euiFormControlPadding: string; euiFormControlCompressedPadding: string; euiFormControlBorderRadius: string; euiFormControlCompressedBorderRadius: string; euiRadioSize: string; euiCheckBoxSize: string; euiCheckboxBorderRadius: string; euiSwitchHeight: string; euiSwitchWidth: string; euiSwitchThumbSize: string; euiSwitchIconHeight: string; euiSwitchHeightCompressed: string; euiSwitchWidthCompressed: string; euiSwitchThumbSizeCompressed: string; euiSwitchHeightMini: string; euiSwitchWidthMini: string; euiSwitchThumbSizeMini: string; euiFormBackgroundColor: string; euiFormBackgroundDisabledColor: string; euiFormBackgroundReadOnlyColor: string; euiFormBorderOpaqueColor: string; euiFormBorderColor: string; euiFormBorderDisabledColor: string; euiFormCustomControlDisabledIconColor: string; euiFormCustomControlBorderColor: string; euiFormControlDisabledColor: string; euiFormControlBoxShadow: string; euiFormControlPlaceholderText: string; euiFormInputGroupLabelBackground: string; euiFormInputGroupBorder: string; euiSwitchOffColor: string; euiFormControlIconSizes: { small: string; medium: string; large: string; xLarge: string; xxLarge: string; }; euiFormControlLayoutGroupInputHeight: string; euiFormControlLayoutGroupInputCompressedHeight: string; euiFormControlLayoutGroupInputCompressedBorderRadius: string; euiRangeTrackColor: string; euiRangeHighlightColor: string; euiRangeThumbRadius: string; euiRangeThumbHeight: string; euiRangeThumbWidth: string; euiRangeThumbBorderColor: string; euiRangeThumbBackgroundColor: string; euiRangeTrackWidth: string; euiRangeTrackHeight: string; euiRangeTrackCompressedHeight: string; euiRangeTrackBorderWidth: number; euiRangeTrackBorderColor: string; euiRangeTrackRadius: string; euiRangeDisabledOpacity: number; euiRangeHighlightHeight: string; euiRangeHighlightCompressedHeight: string; euiRangeHeight: string; euiRangeCompressedHeight: string; euiHeaderBackgroundColor: string; euiHeaderDarkBackgroundColor: string; euiHeaderBorderColor: string; euiHeaderBreadcrumbColor: string; euiHeaderHeight: string; euiHeaderChildSize: string; euiHeaderHeightCompensation: string; euiPageDefaultMaxWidth: string; euiPageSidebarMinWidth: string; euiPanelPaddingModifiers: { paddingSmall: string; paddingMedium: string; paddingLarge: string; }; euiPanelBorderRadiusModifiers: { borderRadiusNone: number; borderRadiusMedium: string; }; euiPanelBackgroundColorModifiers: { transparent: string; plain: string; subdued: string; accent: string; primary: string; success: string; warning: string; danger: string; }; euiBreakpoints: { xs: number; s: string; m: string; l: string; xl: string; }; euiBreakpointKeys: string; euiShadowColor: string; euiSize: string; euiSizeXS: string; euiSizeS: string; euiSizeM: string; euiSizeL: string; euiSizeXL: string; euiSizeXXL: string; euiButtonMinWidth: string; euiScrollBar: string; euiScrollBarCorner: string; euiScrollBarCornerThin: string; euiFocusRingColor: string; euiFocusRingAnimStartColor: string; euiFocusRingAnimStartSize: string; euiFocusRingAnimStartSizeLarge: string; euiFocusRingSizeLarge: string; euiFocusRingSize: string; euiFocusTransparency: number; euiFocusTransparencyPercent: string; euiFocusBackgroundColor: string; euiTooltipBackgroundColor: string; euiTooltipBorderColor: string; euiTooltipAnimations: { top: string; left: string; bottom: string; right: string; }; euiFontFamily: string; euiCodeFontFamily: string; euiFontFeatureSettings: string; euiTextScale: string; euiFontSize: string; euiFontSizeXS: string; euiFontSizeS: string; euiFontSizeM: string; euiFontSizeL: string; euiFontSizeXL: string; euiFontSizeXXL: string; euiLineHeight: number; euiBodyLineHeight: number; euiTitles: { xxxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; s: { 'font-size': string; 'line-height': string; 'font-weight': number; }; m: { 'font-size': string; 'line-height': string; 'font-weight': number; }; l: { 'font-size': string; 'line-height': string; 'font-weight': number; }; }; euiZLevel0: number; euiZLevel1: number; euiZLevel2: number; euiZLevel3: number; euiZLevel4: number; euiZLevel5: number; euiZLevel6: number; euiZLevel7: number; euiZLevel8: number; euiZLevel9: number; euiZToastList: number; euiZModal: number; euiZMask: number; euiZNavigation: number; euiZContentMenu: number; euiZHeader: number; euiZFlyout: number; euiZMaskBelowHeader: number; euiZContent: number; euiColorGhost: string; euiColorInk: string; euiColorPrimary: string; euiColorAccent: string; euiColorSuccess: string; euiColorWarning: string; euiColorDanger: string; euiColorEmptyShade: string; euiColorLightestShade: string; euiColorLightShade: string; euiColorMediumShade: string; euiColorDarkShade: string; euiColorDarkestShade: string; euiColorFullShade: string; euiPageBackgroundColor: string; euiColorHighlight: string; euiTextColor: string; euiTitleColor: string; euiTextSubduedColor: string; euiColorDisabled: string; euiColorPrimaryText: string; euiColorSuccessText: string; euiColorAccentText: string; euiColorWarningText: string; euiColorDangerText: string; euiColorDisabledText: string; euiLinkColor: string; euiColorChartLines: string; euiColorChartBand: string; euiDatePickerCalendarWidth: string; euiDatePickerPadding: string; euiDatePickerGap: string; euiDatePickerCalendarColumns: number; euiDatePickerButtonSize: string; euiDatePickerMinControlWidth: string; euiDatePickerMaxControlWidth: string; euiButtonDefaultTransparency: number; euiButtonFontWeight: number; euiStepStatusColors: { default: string; complete: string; warning: string; danger: string; }; }" + "{ euiZDataGrid: number; euiZHeaderBelowDataGrid: number; euiZDataGridCellPopover: number; euiDataGridCellPaddingS: string; euiDataGridCellPaddingM: string; euiDataGridCellPaddingL: string; euiTableHoverColor: string; euiTableSelectedColor: string; euiTableHoverSelectedColor: string; euiTableActionsBorderColor: string; euiTableHoverClickableColor: string; euiTableFocusClickableColor: string; euiContrastRatioText: number; euiContrastRatioGraphic: number; euiContrastRatioDisabled: number; euiAnimSlightBounce: string; euiAnimSlightResistance: string; euiAnimSpeedExtraFast: string; euiAnimSpeedFast: string; euiAnimSpeedNormal: string; euiAnimSpeedSlow: string; euiAnimSpeedExtraSlow: string; euiBorderWidthThin: string; euiBorderWidthThick: string; euiBorderColor: string; euiBorderRadius: string; euiBorderRadiusSmall: string; euiBorderThick: string; euiBorderThin: string; euiBorderEditable: string; euiButtonHeight: string; euiButtonHeightSmall: string; euiButtonHeightXSmall: string; euiButtonColorDisabled: string; euiButtonColorDisabledText: string; euiButtonColorGhostDisabled: string; euiButtonTypes: { primary: string; accent: string; success: string; warning: string; danger: string; ghost: string; text: string; }; euiPaletteColorBlind: { euiColorVis0: { graphic: string; behindText: string; }; euiColorVis1: { graphic: string; behindText: string; }; euiColorVis2: { graphic: string; behindText: string; }; euiColorVis3: { graphic: string; behindText: string; }; euiColorVis4: { graphic: string; behindText: string; }; euiColorVis5: { graphic: string; behindText: string; }; euiColorVis6: { graphic: string; behindText: string; }; euiColorVis7: { graphic: string; behindText: string; }; euiColorVis8: { graphic: string; behindText: string; }; euiColorVis9: { graphic: string; behindText: string; }; }; euiPaletteColorBlindKeys: string; euiColorVis0: string; euiColorVis1: string; euiColorVis2: string; euiColorVis3: string; euiColorVis4: string; euiColorVis5: string; euiColorVis6: string; euiColorVis7: string; euiColorVis8: string; euiColorVis9: string; euiColorVis0_behindText: string; euiColorVis1_behindText: string; euiColorVis2_behindText: string; euiColorVis3_behindText: string; euiColorVis4_behindText: string; euiColorVis5_behindText: string; euiColorVis6_behindText: string; euiColorVis7_behindText: string; euiColorVis8_behindText: string; euiColorVis9_behindText: string; euiFontWeightLight: number; euiFontWeightRegular: number; euiFontWeightMedium: number; euiFontWeightSemiBold: number; euiFontWeightBold: number; euiCodeFontWeightRegular: number; euiCodeFontWeightBold: number; euiFormMaxWidth: string; euiFormControlHeight: string; euiFormControlCompressedHeight: string; euiFormControlPadding: string; euiFormControlCompressedPadding: string; euiFormControlBorderRadius: string; euiFormControlCompressedBorderRadius: string; euiRadioSize: string; euiCheckBoxSize: string; euiCheckboxBorderRadius: string; euiSwitchHeight: string; euiSwitchWidth: string; euiSwitchThumbSize: string; euiSwitchIconHeight: string; euiSwitchHeightCompressed: string; euiSwitchWidthCompressed: string; euiSwitchThumbSizeCompressed: string; euiSwitchHeightMini: string; euiSwitchWidthMini: string; euiSwitchThumbSizeMini: string; euiFormBackgroundColor: string; euiFormBackgroundDisabledColor: string; euiFormBackgroundReadOnlyColor: string; euiFormBorderOpaqueColor: string; euiFormBorderColor: string; euiFormBorderDisabledColor: string; euiFormCustomControlDisabledIconColor: string; euiFormCustomControlBorderColor: string; euiFormControlDisabledColor: string; euiFormControlBoxShadow: string; euiFormControlPlaceholderText: string; euiFormInputGroupLabelBackground: string; euiFormInputGroupBorder: string; euiSwitchOffColor: string; euiFormControlIconSizes: { small: string; medium: string; large: string; xLarge: string; xxLarge: string; }; euiFormControlLayoutGroupInputHeight: string; euiFormControlLayoutGroupInputCompressedHeight: string; euiFormControlLayoutGroupInputCompressedBorderRadius: string; euiHeaderBackgroundColor: string; euiHeaderDarkBackgroundColor: string; euiHeaderBorderColor: string; euiHeaderBreadcrumbColor: string; euiHeaderHeight: string; euiHeaderChildSize: string; euiHeaderHeightCompensation: string; euiPageDefaultMaxWidth: string; euiPageSidebarMinWidth: string; euiPanelPaddingModifiers: { paddingSmall: string; paddingMedium: string; paddingLarge: string; }; euiPanelBorderRadiusModifiers: { borderRadiusNone: number; borderRadiusMedium: string; }; euiPanelBackgroundColorModifiers: { transparent: string; plain: string; subdued: string; accent: string; primary: string; success: string; warning: string; danger: string; }; euiBreakpoints: { xs: number; s: string; m: string; l: string; xl: string; }; euiBreakpointKeys: string; euiShadowColor: string; euiSize: string; euiSizeXS: string; euiSizeS: string; euiSizeM: string; euiSizeL: string; euiSizeXL: string; euiSizeXXL: string; euiScrollBar: string; euiScrollBarCorner: string; euiScrollBarCornerThin: string; euiFocusRingColor: string; euiFocusRingAnimStartColor: string; euiFocusRingAnimStartSize: string; euiFocusRingAnimStartSizeLarge: string; euiFocusRingSizeLarge: string; euiFocusRingSize: string; euiFocusTransparency: number; euiFocusTransparencyPercent: string; euiFocusBackgroundColor: string; euiFontFamily: string; euiCodeFontFamily: string; euiFontFeatureSettings: string; euiTextScale: string; euiFontSize: string; euiFontSizeXS: string; euiFontSizeS: string; euiFontSizeM: string; euiFontSizeL: string; euiFontSizeXL: string; euiFontSizeXXL: string; euiLineHeight: number; euiBodyLineHeight: number; euiTitles: { xxxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; s: { 'font-size': string; 'line-height': string; 'font-weight': number; }; m: { 'font-size': string; 'line-height': string; 'font-weight': number; }; l: { 'font-size': string; 'line-height': string; 'font-weight': number; }; }; euiZLevel0: number; euiZLevel1: number; euiZLevel2: number; euiZLevel3: number; euiZLevel4: number; euiZLevel5: number; euiZLevel6: number; euiZLevel7: number; euiZLevel8: number; euiZLevel9: number; euiZToastList: number; euiZModal: number; euiZMask: number; euiZNavigation: number; euiZContentMenu: number; euiZHeader: number; euiZFlyout: number; euiZMaskBelowHeader: number; euiZContent: number; euiColorGhost: string; euiColorInk: string; euiColorPrimary: string; euiColorAccent: string; euiColorSuccess: string; euiColorWarning: string; euiColorDanger: string; euiColorEmptyShade: string; euiColorLightestShade: string; euiColorLightShade: string; euiColorMediumShade: string; euiColorDarkShade: string; euiColorDarkestShade: string; euiColorFullShade: string; euiPageBackgroundColor: string; euiColorHighlight: string; euiTextColor: string; euiTitleColor: string; euiTextSubduedColor: string; euiColorDisabled: string; euiColorPrimaryText: string; euiColorSuccessText: string; euiColorAccentText: string; euiColorWarningText: string; euiColorDangerText: string; euiColorDisabledText: string; euiLinkColor: string; euiColorChartLines: string; euiColorChartBand: string; }" ], "path": "packages/kbn-ui-theme/src/theme.ts", "deprecated": false, diff --git a/api_docs/kbn_ui_theme.mdx b/api_docs/kbn_ui_theme.mdx index 7f1b7f8ad5753..3f29f1604e5e7 100644 --- a/api_docs/kbn_ui_theme.mdx +++ b/api_docs/kbn_ui_theme.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-theme title: "@kbn/ui-theme" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-theme plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-theme'] --- import kbnUiThemeObj from './kbn_ui_theme.devdocs.json'; diff --git a/api_docs/kbn_user_profile_components.mdx b/api_docs/kbn_user_profile_components.mdx index 1f9af806acf70..567df8beb13f7 100644 --- a/api_docs/kbn_user_profile_components.mdx +++ b/api_docs/kbn_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-user-profile-components title: "@kbn/user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/user-profile-components plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/user-profile-components'] --- import kbnUserProfileComponentsObj from './kbn_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_utility_types.mdx b/api_docs/kbn_utility_types.mdx index 019d9645672b1..cc1607486ea37 100644 --- a/api_docs/kbn_utility_types.mdx +++ b/api_docs/kbn_utility_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types title: "@kbn/utility-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types'] --- import kbnUtilityTypesObj from './kbn_utility_types.devdocs.json'; diff --git a/api_docs/kbn_utility_types_jest.mdx b/api_docs/kbn_utility_types_jest.mdx index 461696662bc25..e6a428efbd6e3 100644 --- a/api_docs/kbn_utility_types_jest.mdx +++ b/api_docs/kbn_utility_types_jest.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types-jest title: "@kbn/utility-types-jest" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types-jest plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types-jest'] --- import kbnUtilityTypesJestObj from './kbn_utility_types_jest.devdocs.json'; diff --git a/api_docs/kbn_utils.mdx b/api_docs/kbn_utils.mdx index c2198ec85a251..f9cc4ee909919 100644 --- a/api_docs/kbn_utils.mdx +++ b/api_docs/kbn_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utils title: "@kbn/utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utils plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utils'] --- import kbnUtilsObj from './kbn_utils.devdocs.json'; diff --git a/api_docs/kbn_yarn_lock_validator.mdx b/api_docs/kbn_yarn_lock_validator.mdx index e5f6da1a82c4e..fa74b569d62a8 100644 --- a/api_docs/kbn_yarn_lock_validator.mdx +++ b/api_docs/kbn_yarn_lock_validator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-yarn-lock-validator title: "@kbn/yarn-lock-validator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/yarn-lock-validator plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/yarn-lock-validator'] --- import kbnYarnLockValidatorObj from './kbn_yarn_lock_validator.devdocs.json'; diff --git a/api_docs/kibana_overview.mdx b/api_docs/kibana_overview.mdx index 27e4d54fa6ec5..1668e2c19e388 100644 --- a/api_docs/kibana_overview.mdx +++ b/api_docs/kibana_overview.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaOverview title: "kibanaOverview" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaOverview plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaOverview'] --- import kibanaOverviewObj from './kibana_overview.devdocs.json'; diff --git a/api_docs/kibana_react.devdocs.json b/api_docs/kibana_react.devdocs.json index 8ed0bf40a4098..1e2ef711030ac 100644 --- a/api_docs/kibana_react.devdocs.json +++ b/api_docs/kibana_react.devdocs.json @@ -86,7 +86,7 @@ "label": "getDerivedStateFromProps", "description": [], "signature": [ - "(nextProps: Props, prevState: State) => { value: [ValueMember, ValueMember] | undefined; prevValue: [ValueMember, ValueMember] | undefined; isValid: boolean; errorMessage: string; } | null" + "(nextProps: Props, prevState: State) => { value: [string | number, string | number] | undefined; prevValue: [string | number, string | number] | undefined; isValid: boolean; errorMessage: string; } | null" ], "path": "src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx", "deprecated": false, @@ -145,7 +145,7 @@ "label": "_onChange", "description": [], "signature": [ - "(value: [ValueMember, ValueMember]) => void" + "(value: [string | number, string | number]) => void" ], "path": "src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx", "deprecated": false, @@ -159,7 +159,7 @@ "label": "value", "description": [], "signature": [ - "[ValueMember, ValueMember]" + "[string | number, string | number]" ], "path": "src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx", "deprecated": false, @@ -3563,7 +3563,7 @@ "label": "Value", "description": [], "signature": [ - "[ValueMember, ValueMember]" + "[string | number, string | number]" ], "path": "src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx", "deprecated": false, @@ -4119,7 +4119,7 @@ "label": "eui", "description": [], "signature": [ - "{ euiCollapsibleNavGroupLightBackgroundColor: string; euiCollapsibleNavGroupDarkBackgroundColor: string; euiCollapsibleNavGroupDarkHighContrastColor: string; euiColorPickerValueRange0: string; euiColorPickerValueRange1: string; euiColorPickerSaturationRange0: string; euiColorPickerSaturationRange1: string; euiColorPickerIndicatorSize: string; euiColorPickerWidth: string; euiColorPaletteDisplaySizes: { sizeExtraSmall: string; sizeSmall: string; sizeMedium: string; }; euiContextMenuWidth: string; euiControlBarBackground: string; euiControlBarText: string; euiControlBarBorderColor: string; euiControlBarInitialHeight: string; euiControlBarMaxHeight: string; euiControlBarHeights: { s: string; m: string; l: string; }; euiDataGridPrefix: string; euiDataGridStyles: string; euiZDataGrid: number; euiZHeaderBelowDataGrid: number; euiZDataGridCellPopover: number; euiDataGridColumnResizerWidth: string; euiDataGridPopoverMaxHeight: string; euiDataGridCellPaddingS: string; euiDataGridCellPaddingM: string; euiDataGridCellPaddingL: string; euiDataGridVerticalBorder: string; euiSuperDatePickerWidth: string; euiSuperDatePickerButtonWidth: string; euiDragAndDropSpacing: { s: string; m: string; l: string; }; euiEmptyPromptContentMaxWidth: string; euiFilePickerTallHeight: string; euiRangeLevelColors: { primary: string; success: string; warning: string; danger: string; }; textareaResizing: { vertical: string; horizontal: string; both: string; none: string; }; euiHeaderLinksGutterSizes: { gutterXS: string; gutterS: string; gutterM: string; gutterL: string; }; euiKeyPadMenuSize: string; euiKeyPadMenuMarginSize: string; euiMarkdownEditorMinHeight: string; euiResizableButtonTransitionSpeed: string; euiResizableButtonSize: string; euiSelectableListItemBorder: string; euiSelectableListItemPadding: string; euiSelectableTemplateSitewideTypes: { application: { color: string; 'font-weight': number; }; deployment: { color: string; 'font-weight': number; }; article: { color: string; 'font-weight': number; }; case: { color: string; 'font-weight': number; }; platform: { color: string; 'font-weight': number; }; }; euiSideNavEmphasizedBackgroundColor: string; euiSideNavRootTextcolor: string; euiSideNavBranchTextcolor: string; euiSideNavSelectedTextcolor: string; euiSideNavDisabledTextcolor: string; euiStepNumberSize: string; euiStepNumberSmallSize: string; euiStepNumberMargin: string; euiStepStatusColorsToFade: { warning: string; danger: string; disabled: string; incomplete: string; }; euiSuggestItemColors: { tint0: string; tint1: string; tint2: string; tint3: string; tint4: string; tint5: string; tint6: string; tint7: string; tint8: string; tint9: string; tint10: string; }; euiTableCellContentPadding: string; euiTableCellContentPaddingCompressed: string; euiTableCellCheckboxWidth: string; euiTableActionsAreaWidth: string; euiTableHoverColor: string; euiTableSelectedColor: string; euiTableHoverSelectedColor: string; euiTableActionsBorderColor: string; euiTableHoverClickableColor: string; euiTableFocusClickableColor: string; euiContrastRatioText: number; euiContrastRatioGraphic: number; euiContrastRatioDisabled: number; euiAnimSlightBounce: string; euiAnimSlightResistance: string; euiAnimSpeedExtraFast: string; euiAnimSpeedFast: string; euiAnimSpeedNormal: string; euiAnimSpeedSlow: string; euiAnimSpeedExtraSlow: string; euiBorderWidthThin: string; euiBorderWidthThick: string; euiBorderColor: string; euiBorderRadius: string; euiBorderRadiusSmall: string; euiBorderThick: string; euiBorderThin: string; euiBorderEditable: string; euiButtonHeight: string; euiButtonHeightSmall: string; euiButtonHeightXSmall: string; euiButtonColorDisabled: string; euiButtonColorDisabledText: string; euiButtonColorGhostDisabled: string; euiButtonTypes: { primary: string; accent: string; success: string; warning: string; danger: string; ghost: string; text: string; }; euiPaletteColorBlind: { euiColorVis0: { graphic: string; behindText: string; }; euiColorVis1: { graphic: string; behindText: string; }; euiColorVis2: { graphic: string; behindText: string; }; euiColorVis3: { graphic: string; behindText: string; }; euiColorVis4: { graphic: string; behindText: string; }; euiColorVis5: { graphic: string; behindText: string; }; euiColorVis6: { graphic: string; behindText: string; }; euiColorVis7: { graphic: string; behindText: string; }; euiColorVis8: { graphic: string; behindText: string; }; euiColorVis9: { graphic: string; behindText: string; }; }; euiPaletteColorBlindKeys: string; euiColorVis0: string; euiColorVis1: string; euiColorVis2: string; euiColorVis3: string; euiColorVis4: string; euiColorVis5: string; euiColorVis6: string; euiColorVis7: string; euiColorVis8: string; euiColorVis9: string; euiColorVis0_behindText: string; euiColorVis1_behindText: string; euiColorVis2_behindText: string; euiColorVis3_behindText: string; euiColorVis4_behindText: string; euiColorVis5_behindText: string; euiColorVis6_behindText: string; euiColorVis7_behindText: string; euiColorVis8_behindText: string; euiColorVis9_behindText: string; euiFontWeightLight: number; euiFontWeightRegular: number; euiFontWeightMedium: number; euiFontWeightSemiBold: number; euiFontWeightBold: number; euiCodeFontWeightRegular: number; euiCodeFontWeightBold: number; euiFormMaxWidth: string; euiFormControlHeight: string; euiFormControlCompressedHeight: string; euiFormControlPadding: string; euiFormControlCompressedPadding: string; euiFormControlBorderRadius: string; euiFormControlCompressedBorderRadius: string; euiRadioSize: string; euiCheckBoxSize: string; euiCheckboxBorderRadius: string; euiSwitchHeight: string; euiSwitchWidth: string; euiSwitchThumbSize: string; euiSwitchIconHeight: string; euiSwitchHeightCompressed: string; euiSwitchWidthCompressed: string; euiSwitchThumbSizeCompressed: string; euiSwitchHeightMini: string; euiSwitchWidthMini: string; euiSwitchThumbSizeMini: string; euiFormBackgroundColor: string; euiFormBackgroundDisabledColor: string; euiFormBackgroundReadOnlyColor: string; euiFormBorderOpaqueColor: string; euiFormBorderColor: string; euiFormBorderDisabledColor: string; euiFormCustomControlDisabledIconColor: string; euiFormCustomControlBorderColor: string; euiFormControlDisabledColor: string; euiFormControlBoxShadow: string; euiFormControlPlaceholderText: string; euiFormInputGroupLabelBackground: string; euiFormInputGroupBorder: string; euiSwitchOffColor: string; euiFormControlIconSizes: { small: string; medium: string; large: string; xLarge: string; xxLarge: string; }; euiFormControlLayoutGroupInputHeight: string; euiFormControlLayoutGroupInputCompressedHeight: string; euiFormControlLayoutGroupInputCompressedBorderRadius: string; euiRangeTrackColor: string; euiRangeHighlightColor: string; euiRangeThumbRadius: string; euiRangeThumbHeight: string; euiRangeThumbWidth: string; euiRangeThumbBorderColor: string; euiRangeThumbBackgroundColor: string; euiRangeTrackWidth: string; euiRangeTrackHeight: string; euiRangeTrackCompressedHeight: string; euiRangeTrackBorderWidth: number; euiRangeTrackBorderColor: string; euiRangeTrackRadius: string; euiRangeDisabledOpacity: number; euiRangeHighlightHeight: string; euiRangeHighlightCompressedHeight: string; euiRangeHeight: string; euiRangeCompressedHeight: string; euiHeaderBackgroundColor: string; euiHeaderDarkBackgroundColor: string; euiHeaderBorderColor: string; euiHeaderBreadcrumbColor: string; euiHeaderHeight: string; euiHeaderChildSize: string; euiHeaderHeightCompensation: string; euiPageDefaultMaxWidth: string; euiPageSidebarMinWidth: string; euiPanelPaddingModifiers: { paddingSmall: string; paddingMedium: string; paddingLarge: string; }; euiPanelBorderRadiusModifiers: { borderRadiusNone: number; borderRadiusMedium: string; }; euiPanelBackgroundColorModifiers: { transparent: string; plain: string; subdued: string; accent: string; primary: string; success: string; warning: string; danger: string; }; euiBreakpoints: { xs: number; s: string; m: string; l: string; xl: string; }; euiBreakpointKeys: string; euiShadowColor: string; euiSize: string; euiSizeXS: string; euiSizeS: string; euiSizeM: string; euiSizeL: string; euiSizeXL: string; euiSizeXXL: string; euiButtonMinWidth: string; euiScrollBar: string; euiScrollBarCorner: string; euiScrollBarCornerThin: string; euiFocusRingColor: string; euiFocusRingAnimStartColor: string; euiFocusRingAnimStartSize: string; euiFocusRingAnimStartSizeLarge: string; euiFocusRingSizeLarge: string; euiFocusRingSize: string; euiFocusTransparency: number; euiFocusTransparencyPercent: string; euiFocusBackgroundColor: string; euiTooltipBackgroundColor: string; euiTooltipBorderColor: string; euiTooltipAnimations: { top: string; left: string; bottom: string; right: string; }; euiFontFamily: string; euiCodeFontFamily: string; euiFontFeatureSettings: string; euiTextScale: string; euiFontSize: string; euiFontSizeXS: string; euiFontSizeS: string; euiFontSizeM: string; euiFontSizeL: string; euiFontSizeXL: string; euiFontSizeXXL: string; euiLineHeight: number; euiBodyLineHeight: number; euiTitles: { xxxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; s: { 'font-size': string; 'line-height': string; 'font-weight': number; }; m: { 'font-size': string; 'line-height': string; 'font-weight': number; }; l: { 'font-size': string; 'line-height': string; 'font-weight': number; }; }; euiZLevel0: number; euiZLevel1: number; euiZLevel2: number; euiZLevel3: number; euiZLevel4: number; euiZLevel5: number; euiZLevel6: number; euiZLevel7: number; euiZLevel8: number; euiZLevel9: number; euiZToastList: number; euiZModal: number; euiZMask: number; euiZNavigation: number; euiZContentMenu: number; euiZHeader: number; euiZFlyout: number; euiZMaskBelowHeader: number; euiZContent: number; euiColorGhost: string; euiColorInk: string; euiColorPrimary: string; euiColorAccent: string; euiColorSuccess: string; euiColorWarning: string; euiColorDanger: string; euiColorEmptyShade: string; euiColorLightestShade: string; euiColorLightShade: string; euiColorMediumShade: string; euiColorDarkShade: string; euiColorDarkestShade: string; euiColorFullShade: string; euiPageBackgroundColor: string; euiColorHighlight: string; euiTextColor: string; euiTitleColor: string; euiTextSubduedColor: string; euiColorDisabled: string; euiColorPrimaryText: string; euiColorSuccessText: string; euiColorAccentText: string; euiColorWarningText: string; euiColorDangerText: string; euiColorDisabledText: string; euiLinkColor: string; euiColorChartLines: string; euiColorChartBand: string; euiDatePickerCalendarWidth: string; euiDatePickerPadding: string; euiDatePickerGap: string; euiDatePickerCalendarColumns: number; euiDatePickerButtonSize: string; euiDatePickerMinControlWidth: string; euiDatePickerMaxControlWidth: string; euiButtonDefaultTransparency: number; euiButtonFontWeight: number; euiStepStatusColors: { default: string; complete: string; warning: string; danger: string; }; }" + "{ euiZDataGrid: number; euiZHeaderBelowDataGrid: number; euiZDataGridCellPopover: number; euiDataGridCellPaddingS: string; euiDataGridCellPaddingM: string; euiDataGridCellPaddingL: string; euiTableHoverColor: string; euiTableSelectedColor: string; euiTableHoverSelectedColor: string; euiTableActionsBorderColor: string; euiTableHoverClickableColor: string; euiTableFocusClickableColor: string; euiContrastRatioText: number; euiContrastRatioGraphic: number; euiContrastRatioDisabled: number; euiAnimSlightBounce: string; euiAnimSlightResistance: string; euiAnimSpeedExtraFast: string; euiAnimSpeedFast: string; euiAnimSpeedNormal: string; euiAnimSpeedSlow: string; euiAnimSpeedExtraSlow: string; euiBorderWidthThin: string; euiBorderWidthThick: string; euiBorderColor: string; euiBorderRadius: string; euiBorderRadiusSmall: string; euiBorderThick: string; euiBorderThin: string; euiBorderEditable: string; euiButtonHeight: string; euiButtonHeightSmall: string; euiButtonHeightXSmall: string; euiButtonColorDisabled: string; euiButtonColorDisabledText: string; euiButtonColorGhostDisabled: string; euiButtonTypes: { primary: string; accent: string; success: string; warning: string; danger: string; ghost: string; text: string; }; euiPaletteColorBlind: { euiColorVis0: { graphic: string; behindText: string; }; euiColorVis1: { graphic: string; behindText: string; }; euiColorVis2: { graphic: string; behindText: string; }; euiColorVis3: { graphic: string; behindText: string; }; euiColorVis4: { graphic: string; behindText: string; }; euiColorVis5: { graphic: string; behindText: string; }; euiColorVis6: { graphic: string; behindText: string; }; euiColorVis7: { graphic: string; behindText: string; }; euiColorVis8: { graphic: string; behindText: string; }; euiColorVis9: { graphic: string; behindText: string; }; }; euiPaletteColorBlindKeys: string; euiColorVis0: string; euiColorVis1: string; euiColorVis2: string; euiColorVis3: string; euiColorVis4: string; euiColorVis5: string; euiColorVis6: string; euiColorVis7: string; euiColorVis8: string; euiColorVis9: string; euiColorVis0_behindText: string; euiColorVis1_behindText: string; euiColorVis2_behindText: string; euiColorVis3_behindText: string; euiColorVis4_behindText: string; euiColorVis5_behindText: string; euiColorVis6_behindText: string; euiColorVis7_behindText: string; euiColorVis8_behindText: string; euiColorVis9_behindText: string; euiFontWeightLight: number; euiFontWeightRegular: number; euiFontWeightMedium: number; euiFontWeightSemiBold: number; euiFontWeightBold: number; euiCodeFontWeightRegular: number; euiCodeFontWeightBold: number; euiFormMaxWidth: string; euiFormControlHeight: string; euiFormControlCompressedHeight: string; euiFormControlPadding: string; euiFormControlCompressedPadding: string; euiFormControlBorderRadius: string; euiFormControlCompressedBorderRadius: string; euiRadioSize: string; euiCheckBoxSize: string; euiCheckboxBorderRadius: string; euiSwitchHeight: string; euiSwitchWidth: string; euiSwitchThumbSize: string; euiSwitchIconHeight: string; euiSwitchHeightCompressed: string; euiSwitchWidthCompressed: string; euiSwitchThumbSizeCompressed: string; euiSwitchHeightMini: string; euiSwitchWidthMini: string; euiSwitchThumbSizeMini: string; euiFormBackgroundColor: string; euiFormBackgroundDisabledColor: string; euiFormBackgroundReadOnlyColor: string; euiFormBorderOpaqueColor: string; euiFormBorderColor: string; euiFormBorderDisabledColor: string; euiFormCustomControlDisabledIconColor: string; euiFormCustomControlBorderColor: string; euiFormControlDisabledColor: string; euiFormControlBoxShadow: string; euiFormControlPlaceholderText: string; euiFormInputGroupLabelBackground: string; euiFormInputGroupBorder: string; euiSwitchOffColor: string; euiFormControlIconSizes: { small: string; medium: string; large: string; xLarge: string; xxLarge: string; }; euiFormControlLayoutGroupInputHeight: string; euiFormControlLayoutGroupInputCompressedHeight: string; euiFormControlLayoutGroupInputCompressedBorderRadius: string; euiHeaderBackgroundColor: string; euiHeaderDarkBackgroundColor: string; euiHeaderBorderColor: string; euiHeaderBreadcrumbColor: string; euiHeaderHeight: string; euiHeaderChildSize: string; euiHeaderHeightCompensation: string; euiPageDefaultMaxWidth: string; euiPageSidebarMinWidth: string; euiPanelPaddingModifiers: { paddingSmall: string; paddingMedium: string; paddingLarge: string; }; euiPanelBorderRadiusModifiers: { borderRadiusNone: number; borderRadiusMedium: string; }; euiPanelBackgroundColorModifiers: { transparent: string; plain: string; subdued: string; accent: string; primary: string; success: string; warning: string; danger: string; }; euiBreakpoints: { xs: number; s: string; m: string; l: string; xl: string; }; euiBreakpointKeys: string; euiShadowColor: string; euiSize: string; euiSizeXS: string; euiSizeS: string; euiSizeM: string; euiSizeL: string; euiSizeXL: string; euiSizeXXL: string; euiScrollBar: string; euiScrollBarCorner: string; euiScrollBarCornerThin: string; euiFocusRingColor: string; euiFocusRingAnimStartColor: string; euiFocusRingAnimStartSize: string; euiFocusRingAnimStartSizeLarge: string; euiFocusRingSizeLarge: string; euiFocusRingSize: string; euiFocusTransparency: number; euiFocusTransparencyPercent: string; euiFocusBackgroundColor: string; euiFontFamily: string; euiCodeFontFamily: string; euiFontFeatureSettings: string; euiTextScale: string; euiFontSize: string; euiFontSizeXS: string; euiFontSizeS: string; euiFontSizeM: string; euiFontSizeL: string; euiFontSizeXL: string; euiFontSizeXXL: string; euiLineHeight: number; euiBodyLineHeight: number; euiTitles: { xxxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xxs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; xs: { 'font-size': string; 'line-height': string; 'font-weight': number; }; s: { 'font-size': string; 'line-height': string; 'font-weight': number; }; m: { 'font-size': string; 'line-height': string; 'font-weight': number; }; l: { 'font-size': string; 'line-height': string; 'font-weight': number; }; }; euiZLevel0: number; euiZLevel1: number; euiZLevel2: number; euiZLevel3: number; euiZLevel4: number; euiZLevel5: number; euiZLevel6: number; euiZLevel7: number; euiZLevel8: number; euiZLevel9: number; euiZToastList: number; euiZModal: number; euiZMask: number; euiZNavigation: number; euiZContentMenu: number; euiZHeader: number; euiZFlyout: number; euiZMaskBelowHeader: number; euiZContent: number; euiColorGhost: string; euiColorInk: string; euiColorPrimary: string; euiColorAccent: string; euiColorSuccess: string; euiColorWarning: string; euiColorDanger: string; euiColorEmptyShade: string; euiColorLightestShade: string; euiColorLightShade: string; euiColorMediumShade: string; euiColorDarkShade: string; euiColorDarkestShade: string; euiColorFullShade: string; euiPageBackgroundColor: string; euiColorHighlight: string; euiTextColor: string; euiTitleColor: string; euiTextSubduedColor: string; euiColorDisabled: string; euiColorPrimaryText: string; euiColorSuccessText: string; euiColorAccentText: string; euiColorWarningText: string; euiColorDangerText: string; euiColorDisabledText: string; euiLinkColor: string; euiColorChartLines: string; euiColorChartBand: string; }" ], "path": "src/plugins/kibana_react/common/eui_styled_components.tsx", "deprecated": false, diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index 210675dae533c..04ba2492e3cb1 100644 --- a/api_docs/kibana_react.mdx +++ b/api_docs/kibana_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaReact title: "kibanaReact" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaReact plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaReact'] --- import kibanaReactObj from './kibana_react.devdocs.json'; diff --git a/api_docs/kibana_utils.mdx b/api_docs/kibana_utils.mdx index 1b278651cb7d6..f38a6ac386bb9 100644 --- a/api_docs/kibana_utils.mdx +++ b/api_docs/kibana_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaUtils title: "kibanaUtils" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaUtils plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaUtils'] --- import kibanaUtilsObj from './kibana_utils.devdocs.json'; diff --git a/api_docs/kubernetes_security.mdx b/api_docs/kubernetes_security.mdx index 893ea884909bc..0154ec63980d6 100644 --- a/api_docs/kubernetes_security.mdx +++ b/api_docs/kubernetes_security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kubernetesSecurity title: "kubernetesSecurity" image: https://source.unsplash.com/400x175/?github description: API docs for the kubernetesSecurity plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kubernetesSecurity'] --- import kubernetesSecurityObj from './kubernetes_security.devdocs.json'; diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index c77cfd90650af..ab0082ec490c6 100644 --- a/api_docs/lens.mdx +++ b/api_docs/lens.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lens title: "lens" image: https://source.unsplash.com/400x175/?github description: API docs for the lens plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lens'] --- import lensObj from './lens.devdocs.json'; diff --git a/api_docs/license_api_guard.mdx b/api_docs/license_api_guard.mdx index fb0e02d2ad2ce..7233272b18b75 100644 --- a/api_docs/license_api_guard.mdx +++ b/api_docs/license_api_guard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseApiGuard title: "licenseApiGuard" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseApiGuard plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseApiGuard'] --- import licenseApiGuardObj from './license_api_guard.devdocs.json'; diff --git a/api_docs/license_management.mdx b/api_docs/license_management.mdx index c7c658d2dd271..aa4cd0b204d17 100644 --- a/api_docs/license_management.mdx +++ b/api_docs/license_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseManagement title: "licenseManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseManagement plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseManagement'] --- import licenseManagementObj from './license_management.devdocs.json'; diff --git a/api_docs/licensing.mdx b/api_docs/licensing.mdx index 340af45ca3dde..19d2c18fb5508 100644 --- a/api_docs/licensing.mdx +++ b/api_docs/licensing.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licensing title: "licensing" image: https://source.unsplash.com/400x175/?github description: API docs for the licensing plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licensing'] --- import licensingObj from './licensing.devdocs.json'; diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index 73f376c20957e..2be124a812070 100644 --- a/api_docs/lists.mdx +++ b/api_docs/lists.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lists title: "lists" image: https://source.unsplash.com/400x175/?github description: API docs for the lists plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lists'] --- import listsObj from './lists.devdocs.json'; diff --git a/api_docs/management.mdx b/api_docs/management.mdx index 2d4879b74b7f2..3e89378a3bf25 100644 --- a/api_docs/management.mdx +++ b/api_docs/management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/management title: "management" image: https://source.unsplash.com/400x175/?github description: API docs for the management plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'management'] --- import managementObj from './management.devdocs.json'; diff --git a/api_docs/maps.mdx b/api_docs/maps.mdx index 0c7b2b21621c2..f8225dea0d7e8 100644 --- a/api_docs/maps.mdx +++ b/api_docs/maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/maps title: "maps" image: https://source.unsplash.com/400x175/?github description: API docs for the maps plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'maps'] --- import mapsObj from './maps.devdocs.json'; diff --git a/api_docs/maps_ems.mdx b/api_docs/maps_ems.mdx index d22af86f594be..d06582160fb9c 100644 --- a/api_docs/maps_ems.mdx +++ b/api_docs/maps_ems.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/mapsEms title: "mapsEms" image: https://source.unsplash.com/400x175/?github description: API docs for the mapsEms plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mapsEms'] --- import mapsEmsObj from './maps_ems.devdocs.json'; diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx index b9869a16cb43e..7c600d444763f 100644 --- a/api_docs/ml.mdx +++ b/api_docs/ml.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ml title: "ml" image: https://source.unsplash.com/400x175/?github description: API docs for the ml plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ml'] --- import mlObj from './ml.devdocs.json'; diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx index b41f347a51d94..87b71a8a8f053 100644 --- a/api_docs/monitoring.mdx +++ b/api_docs/monitoring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoring title: "monitoring" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoring plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoring'] --- import monitoringObj from './monitoring.devdocs.json'; diff --git a/api_docs/monitoring_collection.mdx b/api_docs/monitoring_collection.mdx index 1919198e89cc9..356db6d5ebe4c 100644 --- a/api_docs/monitoring_collection.mdx +++ b/api_docs/monitoring_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoringCollection title: "monitoringCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoringCollection plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoringCollection'] --- import monitoringCollectionObj from './monitoring_collection.devdocs.json'; diff --git a/api_docs/navigation.mdx b/api_docs/navigation.mdx index 2fbda74a9425a..2a79e80c18b77 100644 --- a/api_docs/navigation.mdx +++ b/api_docs/navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/navigation title: "navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the navigation plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'navigation'] --- import navigationObj from './navigation.devdocs.json'; diff --git a/api_docs/newsfeed.mdx b/api_docs/newsfeed.mdx index 5e46a2f57e74e..00365297817eb 100644 --- a/api_docs/newsfeed.mdx +++ b/api_docs/newsfeed.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/newsfeed title: "newsfeed" image: https://source.unsplash.com/400x175/?github description: API docs for the newsfeed plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed'] --- import newsfeedObj from './newsfeed.devdocs.json'; diff --git a/api_docs/notifications.mdx b/api_docs/notifications.mdx index c494d12518e73..26cbb324264d8 100644 --- a/api_docs/notifications.mdx +++ b/api_docs/notifications.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/notifications title: "notifications" image: https://source.unsplash.com/400x175/?github description: API docs for the notifications plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'notifications'] --- import notificationsObj from './notifications.devdocs.json'; diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index b282a4dcdd668..1250035864b96 100644 --- a/api_docs/observability.mdx +++ b/api_docs/observability.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observability title: "observability" image: https://source.unsplash.com/400x175/?github description: API docs for the observability plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability'] --- import observabilityObj from './observability.devdocs.json'; diff --git a/api_docs/osquery.mdx b/api_docs/osquery.mdx index 273588ca86018..dbadf65100ec0 100644 --- a/api_docs/osquery.mdx +++ b/api_docs/osquery.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/osquery title: "osquery" image: https://source.unsplash.com/400x175/?github description: API docs for the osquery plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'osquery'] --- import osqueryObj from './osquery.devdocs.json'; diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index a90f4910e9460..2077920918a34 100644 --- a/api_docs/plugin_directory.mdx +++ b/api_docs/plugin_directory.mdx @@ -7,7 +7,7 @@ id: kibDevDocsPluginDirectory slug: /kibana-dev-docs/api-meta/plugin-api-directory title: Directory description: Directory of public APIs available through plugins or packages. -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -15,13 +15,13 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Count | Plugins or Packages with a
    public API | Number of teams | |--------------|----------|------------------------| -| 529 | 441 | 41 | +| 532 | 444 | 42 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 33829 | 523 | 23585 | 1159 | +| 33897 | 524 | 23628 | 1159 | ## Plugin Directory @@ -30,7 +30,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 226 | 8 | 221 | 24 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 36 | 1 | 32 | 2 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | AIOps plugin maintained by ML team. | 12 | 0 | 1 | 2 | -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 420 | 0 | 411 | 35 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 421 | 0 | 412 | 35 | | | [APM UI](https://github.com/orgs/elastic/teams/apm-ui) | The user interface for Elastic APM | 42 | 0 | 42 | 58 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 9 | 0 | 9 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Considering using bfetch capabilities when fetching large amounts of data. This services supports batching HTTP requests and streaming responses back. | 89 | 1 | 74 | 2 | @@ -40,14 +40,15 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 39 | 0 | 11 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | Chat available on Elastic Cloud deployments for quicker assistance. | 1 | 0 | 0 | 0 | | | [Platform Onboarding](https://github.com/orgs/elastic/teams/platform-onboarding) | Static migration page where self-managed users can see text/copy about migrating to Elastic Cloud | 7 | 1 | 7 | 1 | +| | [Cloud Native Integrations](https://github.com/orgs/elastic/teams/sec-cloudnative-integrations) | Defend for Containers | 2 | 0 | 2 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/@elastic/kibana-core) | Provides the necessary APIs to implement A/B testing scenarios, fetching the variations in configuration and reporting back metrics to track conversion rates of the experiments. | 12 | 0 | 0 | 0 | | cloudFullStory | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | When Kibana runs on Elastic Cloud, this plugin registers FullStory as a shipper for telemetry. | 0 | 0 | 0 | 0 | | cloudGainsight | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | When Kibana runs on Elastic Cloud, this plugin registers Gainsight as a shipper for telemetry. | 0 | 0 | 0 | 0 | | cloudLinks | [Kibana Core](https://github.com/orgs/elastic/teams/@kibana-core) | Adds the links to the Elastic Cloud console | 0 | 0 | 0 | 0 | | | [Cloud Security Posture](https://github.com/orgs/elastic/teams/cloud-posture-security) | The cloud security posture plugin | 17 | 0 | 2 | 2 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 13 | 0 | 13 | 1 | -| | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Controls Plugin contains embeddable components intended to create a simple query interface for end users, and a powerful editing suite that allows dashboard authors to build controls | 268 | 0 | 259 | 10 | -| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2809 | 17 | 1013 | 0 | +| | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Controls Plugin contains embeddable components intended to create a simple query interface for end users, and a powerful editing suite that allows dashboard authors to build controls | 271 | 0 | 262 | 10 | +| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2811 | 17 | 1014 | 0 | | crossClusterReplication | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | customBranding | [global-experience](https://github.com/orgs/elastic/teams/kibana-global-experience) | Enables customization of Kibana | 0 | 0 | 0 | 0 | | | [Fleet](https://github.com/orgs/elastic/teams/fleet) | Add custom data integrations so they can be displayed in the Fleet integrations app | 107 | 0 | 88 | 1 | @@ -62,7 +63,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 10 | 0 | 8 | 2 | | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the Discover application and the saved search embeddable. | 100 | 0 | 82 | 4 | | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 37 | 0 | 35 | 2 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds embeddables service to Kibana | 522 | 8 | 421 | 4 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds embeddables service to Kibana | 524 | 8 | 423 | 4 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Extends embeddable plugin with more functionality | 14 | 0 | 14 | 0 | | | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides encryption and decryption utilities for saved objects containing sensitive information. | 51 | 0 | 44 | 0 | | | [Enterprise Search](https://github.com/orgs/elastic/teams/enterprise-search-frontend) | Adds dashboards for discovering and managing Enterprise Search products. | 9 | 0 | 9 | 0 | @@ -96,6 +97,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | grokdebugger | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | | [Journey Onboarding](https://github.com/orgs/elastic/teams/platform-onboarding) | Guided onboarding framework | 103 | 0 | 102 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 143 | 0 | 104 | 0 | +| imageEmbeddable | [@elastic/kibana-global-experience](https://github.com/orgs/elastic/teams/@elastic/kibana-global-experience) | Image embeddable | 0 | 0 | 0 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 4 | 0 | 4 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 177 | 0 | 172 | 3 | | | [Logs and Metrics UI](https://github.com/orgs/elastic/teams/logs-metrics-ui) | This plugin visualizes data from Filebeat and Metricbeat, and integrates with other Observability solutions | 42 | 0 | 39 | 8 | @@ -126,7 +128,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Observability UI](https://github.com/orgs/elastic/teams/observability-ui) | - | 586 | 40 | 582 | 32 | | | [Security asset management](https://github.com/orgs/elastic/teams/security-asset-management) | - | 21 | 0 | 21 | 5 | | painlessLab | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | -| | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas). | 230 | 7 | 174 | 11 | +| | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas). | 231 | 7 | 175 | 11 | | | [profiling](https://github.com/orgs/elastic/teams/profiling-ui) | - | 15 | 2 | 15 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 4 | 0 | 4 | 0 | | | [Kibana Reporting Services](https://github.com/orgs/elastic/teams/kibana-reporting-services) | Reporting Services enables applications to feature reports that the user can automate with Watcher and download later. | 36 | 0 | 16 | 0 | @@ -137,12 +139,12 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 16 | 0 | 16 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 130 | 0 | 117 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 79 | 0 | 73 | 3 | -| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 98 | 0 | 50 | 1 | +| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 100 | 0 | 52 | 1 | | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the definition and helper methods around saved searches, used by discover and visualizations. | 45 | 0 | 45 | 1 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 32 | 0 | 13 | 0 | | | [Kibana Reporting Services](https://github.com/orgs/elastic/teams/kibana-reporting-services) | Kibana Screenshotting Plugin | 27 | 0 | 8 | 4 | | searchprofiler | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | -| | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides authentication and authorization features, and exposes functionality to understand the capabilities of the currently authenticated user. | 252 | 0 | 92 | 3 | +| | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides authentication and authorization features, and exposes functionality to understand the capabilities of the currently authenticated user. | 251 | 0 | 91 | 1 | | | [Security solution](https://github.com/orgs/elastic/teams/security-solution) | - | 112 | 0 | 75 | 28 | | | [Security Team](https://github.com/orgs/elastic/teams/security-team) | - | 7 | 0 | 7 | 1 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds URL Service and sharing capabilities to Kibana | 115 | 0 | 56 | 10 | @@ -160,12 +162,12 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Security solution](https://github.com/orgs/elastic/teams/security-solution) | - | 257 | 1 | 214 | 21 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the transforms features provided by Elastic. Transforms enable you to convert existing Elasticsearch indices into summarized indices, which provide opportunities for new insights and analytics. | 4 | 0 | 4 | 1 | | translations | [Kibana Localization](https://github.com/orgs/elastic/teams/kibana-localization) | - | 0 | 0 | 0 | 0 | -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 576 | 11 | 547 | 52 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 579 | 11 | 550 | 53 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds UI Actions service to Kibana | 135 | 2 | 92 | 11 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Extends UI Actions plugin with more functionality | 206 | 0 | 142 | 9 | | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the field list which can be integrated into apps | 215 | 0 | 203 | 7 | | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | The `unifiedHistogram` plugin provides UI components to create a layout including a resizable histogram and a main display. | 52 | 0 | 15 | 0 | -| | [Visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | Contains all the key functionality of Kibana's unified search experience.Contains all the key functionality of Kibana's unified search experience. | 134 | 2 | 106 | 18 | +| | [Visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | Contains all the key functionality of Kibana's unified search experience.Contains all the key functionality of Kibana's unified search experience. | 134 | 2 | 105 | 18 | | upgradeAssistant | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | urlDrilldown | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds drilldown implementations to Kibana | 0 | 0 | 0 | 0 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 12 | 0 | 12 | 0 | @@ -280,7 +282,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | Kibana Core | - | 7 | 0 | 7 | 0 | | | Kibana Core | - | 25 | 5 | 25 | 1 | | | Kibana Core | - | 7 | 0 | 7 | 1 | -| | Kibana Core | - | 393 | 1 | 154 | 0 | +| | Kibana Core | - | 392 | 1 | 154 | 0 | | | Kibana Core | - | 54 | 0 | 48 | 6 | | | Kibana Core | - | 41 | 0 | 40 | 0 | | | Kibana Core | - | 4 | 0 | 2 | 0 | @@ -340,10 +342,10 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | Kibana Core | - | 4 | 0 | 4 | 0 | | | Kibana Core | - | 112 | 0 | 79 | 45 | | | Kibana Core | - | 12 | 0 | 12 | 0 | -| | Kibana Core | - | 297 | 0 | 90 | 0 | +| | Kibana Core | - | 305 | 0 | 91 | 0 | | | Kibana Core | - | 69 | 0 | 69 | 4 | | | Kibana Core | - | 14 | 0 | 14 | 0 | -| | Kibana Core | - | 99 | 1 | 84 | 0 | +| | Kibana Core | - | 108 | 1 | 84 | 0 | | | Kibana Core | - | 12 | 0 | 2 | 0 | | | Kibana Core | - | 19 | 0 | 18 | 0 | | | Kibana Core | - | 20 | 0 | 3 | 0 | @@ -381,7 +383,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | - | 4 | 0 | 4 | 0 | | | [Owner missing] | - | 27 | 0 | 14 | 1 | | | Kibana Core | - | 7 | 0 | 3 | 0 | -| | [Owner missing] | - | 227 | 1 | 170 | 13 | +| | [Owner missing] | - | 244 | 2 | 187 | 13 | | | Kibana Core | - | 11 | 0 | 11 | 0 | | | [Owner missing] | - | 2 | 0 | 1 | 0 | | | [Owner missing] | - | 20 | 0 | 16 | 0 | @@ -411,7 +413,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | Machine Learning UI | This package includes utility functions related to creating elasticsearch aggregation queries, data manipulation and verification. | 82 | 2 | 58 | 0 | | | Machine Learning UI | A type guard to check record like object structures. | 3 | 0 | 2 | 0 | | | Machine Learning UI | Creates a deterministic number based hash out of a string. | 2 | 0 | 1 | 0 | -| | [Owner missing] | - | 60 | 0 | 60 | 2 | +| | [Owner missing] | - | 62 | 0 | 60 | 2 | | | [Owner missing] | - | 47 | 0 | 46 | 10 | | | [Owner missing] | - | 51 | 5 | 34 | 0 | | | [Owner missing] | io ts utilities and types to be shared with plugins from the osquery project | 62 | 0 | 62 | 0 | @@ -452,6 +454,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [Owner missing] | - | 3 | 0 | 2 | 0 | | | [Owner missing] | - | 2 | 0 | 2 | 0 | | | [Owner missing] | - | 1 | 0 | 1 | 0 | +| | [Owner missing] | - | 11 | 0 | 5 | 0 | +| | [Owner missing] | - | 7 | 0 | 7 | 1 | | | [Owner missing] | - | 17 | 0 | 15 | 0 | | | [Owner missing] | - | 17 | 0 | 9 | 0 | | | [Owner missing] | - | 10 | 0 | 9 | 0 | diff --git a/api_docs/presentation_util.devdocs.json b/api_docs/presentation_util.devdocs.json index a14f737f80fab..3c39d2ff5f862 100644 --- a/api_docs/presentation_util.devdocs.json +++ b/api_docs/presentation_util.devdocs.json @@ -659,6 +659,23 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "presentationUtil", + "id": "def-public.getContextProvider", + "type": "Function", + "tags": [], + "label": "getContextProvider", + "description": [], + "signature": [ + "() => React.FC<{}>" + ], + "path": "src/plugins/presentation_util/public/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "presentationUtil", "id": "def-public.LazyDashboardPicker", diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index f914ebf4b5816..842d073ea083c 100644 --- a/api_docs/presentation_util.mdx +++ b/api_docs/presentation_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/presentationUtil title: "presentationUtil" image: https://source.unsplash.com/400x175/?github description: API docs for the presentationUtil plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'presentationUtil'] --- import presentationUtilObj from './presentation_util.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-prese | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 230 | 7 | 174 | 11 | +| 231 | 7 | 175 | 11 | ## Client diff --git a/api_docs/profiling.mdx b/api_docs/profiling.mdx index d874c9447f019..ce53dc1436180 100644 --- a/api_docs/profiling.mdx +++ b/api_docs/profiling.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/profiling title: "profiling" image: https://source.unsplash.com/400x175/?github description: API docs for the profiling plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profiling'] --- import profilingObj from './profiling.devdocs.json'; diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx index 23736a6a1edc7..af00bf983f9ac 100644 --- a/api_docs/remote_clusters.mdx +++ b/api_docs/remote_clusters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/remoteClusters title: "remoteClusters" image: https://source.unsplash.com/400x175/?github description: API docs for the remoteClusters plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'remoteClusters'] --- import remoteClustersObj from './remote_clusters.devdocs.json'; diff --git a/api_docs/reporting.mdx b/api_docs/reporting.mdx index fe6c6622a86c5..d38c7472ee325 100644 --- a/api_docs/reporting.mdx +++ b/api_docs/reporting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/reporting title: "reporting" image: https://source.unsplash.com/400x175/?github description: API docs for the reporting plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'reporting'] --- import reportingObj from './reporting.devdocs.json'; diff --git a/api_docs/rollup.mdx b/api_docs/rollup.mdx index 2f20a7bdef0a6..2d6a138957a16 100644 --- a/api_docs/rollup.mdx +++ b/api_docs/rollup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/rollup title: "rollup" image: https://source.unsplash.com/400x175/?github description: API docs for the rollup plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'rollup'] --- import rollupObj from './rollup.devdocs.json'; diff --git a/api_docs/rule_registry.devdocs.json b/api_docs/rule_registry.devdocs.json index 22f1edf758d6e..db3d9e7022e33 100644 --- a/api_docs/rule_registry.devdocs.json +++ b/api_docs/rule_registry.devdocs.json @@ -1448,7 +1448,7 @@ "label": "createGetSummarizedAlertsFn", "description": [], "signature": [ - "(opts: CreateGetSummarizedAlertsFnOpts) => () => ({ start, end, executionUuid, ruleId, spaceId }: ", + "(opts: CreateGetSummarizedAlertsFnOpts) => () => ({ start, end, executionUuid, ruleId, spaceId, excludedAlertInstanceIds, }: ", { "pluginId": "alerting", "scope": "server", @@ -1793,7 +1793,7 @@ "section": "def-common.WithoutReservedActionGroups", "text": "WithoutReservedActionGroups" }, - ">) => Promise; getSummarizedAlerts: ({ start, end, executionUuid, ruleId, spaceId }: ", + ">) => Promise; getSummarizedAlerts: ({ start, end, executionUuid, ruleId, spaceId, excludedAlertInstanceIds, }: ", { "pluginId": "alerting", "scope": "server", diff --git a/api_docs/rule_registry.mdx b/api_docs/rule_registry.mdx index 228f54fc5cbff..cb4326f2cf36e 100644 --- a/api_docs/rule_registry.mdx +++ b/api_docs/rule_registry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ruleRegistry title: "ruleRegistry" image: https://source.unsplash.com/400x175/?github description: API docs for the ruleRegistry plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ruleRegistry'] --- import ruleRegistryObj from './rule_registry.devdocs.json'; diff --git a/api_docs/runtime_fields.mdx b/api_docs/runtime_fields.mdx index 7b0c296f37187..36822f91ea186 100644 --- a/api_docs/runtime_fields.mdx +++ b/api_docs/runtime_fields.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/runtimeFields title: "runtimeFields" image: https://source.unsplash.com/400x175/?github description: API docs for the runtimeFields plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'runtimeFields'] --- import runtimeFieldsObj from './runtime_fields.devdocs.json'; diff --git a/api_docs/saved_objects.mdx b/api_docs/saved_objects.mdx index b5bf08de16456..aa73be71b2d15 100644 --- a/api_docs/saved_objects.mdx +++ b/api_docs/saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjects title: "savedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjects plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjects'] --- import savedObjectsObj from './saved_objects.devdocs.json'; diff --git a/api_docs/saved_objects_finder.mdx b/api_docs/saved_objects_finder.mdx index 8c84049a57e44..ba27dfc3a069b 100644 --- a/api_docs/saved_objects_finder.mdx +++ b/api_docs/saved_objects_finder.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsFinder title: "savedObjectsFinder" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsFinder plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsFinder'] --- import savedObjectsFinderObj from './saved_objects_finder.devdocs.json'; diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx index facadee4347c2..b32e2bcd5934d 100644 --- a/api_docs/saved_objects_management.mdx +++ b/api_docs/saved_objects_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsManagement title: "savedObjectsManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsManagement plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsManagement'] --- import savedObjectsManagementObj from './saved_objects_management.devdocs.json'; diff --git a/api_docs/saved_objects_tagging.mdx b/api_docs/saved_objects_tagging.mdx index 506abc7bf659b..66a16fa867ca4 100644 --- a/api_docs/saved_objects_tagging.mdx +++ b/api_docs/saved_objects_tagging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTagging title: "savedObjectsTagging" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTagging plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTagging'] --- import savedObjectsTaggingObj from './saved_objects_tagging.devdocs.json'; diff --git a/api_docs/saved_objects_tagging_oss.devdocs.json b/api_docs/saved_objects_tagging_oss.devdocs.json index 0b0a504b7fe2c..05bc556ebdf99 100644 --- a/api_docs/saved_objects_tagging_oss.devdocs.json +++ b/api_docs/saved_objects_tagging_oss.devdocs.json @@ -131,7 +131,7 @@ "\nReturn an observable that will emit everytime the cache's state mutates." ], "signature": [ - "() => ", + "(params?: { waitForInitialization?: boolean | undefined; } | undefined) => ", "Observable", "<", { @@ -146,7 +146,35 @@ "path": "src/plugins/saved_objects_tagging_oss/public/api.ts", "deprecated": false, "trackAdoption": false, - "children": [], + "children": [ + { + "parentPluginId": "savedObjectsTaggingOss", + "id": "def-public.ITagsCache.getState$.$1", + "type": "Object", + "tags": [], + "label": "params", + "description": [], + "path": "src/plugins/saved_objects_tagging_oss/public/api.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "savedObjectsTaggingOss", + "id": "def-public.ITagsCache.getState$.$1.waitForInitialization", + "type": "CompoundType", + "tags": [], + "label": "waitForInitialization", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/saved_objects_tagging_oss/public/api.ts", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], "returnComment": [] } ], @@ -680,14 +708,15 @@ "section": "def-public.ParseSearchQueryOptions", "text": "ParseSearchQueryOptions" }, - " | undefined) => ", + " | undefined) => Promise<", { "pluginId": "savedObjectsTaggingOss", "scope": "public", "docId": "kibSavedObjectsTaggingOssPluginApi", "section": "def-public.ParsedSearchQuery", "text": "ParsedSearchQuery" - } + }, + ">" ], "path": "src/plugins/saved_objects_tagging_oss/public/api.ts", "deprecated": false, diff --git a/api_docs/saved_objects_tagging_oss.mdx b/api_docs/saved_objects_tagging_oss.mdx index 3670d0ae7ca59..a4a9b8600a246 100644 --- a/api_docs/saved_objects_tagging_oss.mdx +++ b/api_docs/saved_objects_tagging_oss.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTaggingOss title: "savedObjectsTaggingOss" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTaggingOss plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTaggingOss'] --- import savedObjectsTaggingOssObj from './saved_objects_tagging_oss.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 98 | 0 | 50 | 1 | +| 100 | 0 | 52 | 1 | ## Client diff --git a/api_docs/saved_search.mdx b/api_docs/saved_search.mdx index 849b44c1b6747..1c4910fa0a7bd 100644 --- a/api_docs/saved_search.mdx +++ b/api_docs/saved_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedSearch title: "savedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the savedSearch plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedSearch'] --- import savedSearchObj from './saved_search.devdocs.json'; diff --git a/api_docs/screenshot_mode.mdx b/api_docs/screenshot_mode.mdx index 755908981302a..01c0d1e5a36fe 100644 --- a/api_docs/screenshot_mode.mdx +++ b/api_docs/screenshot_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotMode title: "screenshotMode" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotMode plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotMode'] --- import screenshotModeObj from './screenshot_mode.devdocs.json'; diff --git a/api_docs/screenshotting.mdx b/api_docs/screenshotting.mdx index 0e6ed137d6cb2..f7df1151cea61 100644 --- a/api_docs/screenshotting.mdx +++ b/api_docs/screenshotting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotting title: "screenshotting" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotting plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotting'] --- import screenshottingObj from './screenshotting.devdocs.json'; diff --git a/api_docs/security.devdocs.json b/api_docs/security.devdocs.json index 60421e61a75a0..ec61dc141f3b7 100644 --- a/api_docs/security.devdocs.json +++ b/api_docs/security.devdocs.json @@ -1258,23 +1258,7 @@ "label": "kibana", "description": [], "signature": [ - "AuditKibana", - " | undefined" - ], - "path": "x-pack/plugins/security/server/audit/audit_events.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "security", - "id": "def-server.AuditEvent.http", - "type": "Object", - "tags": [], - "label": "http", - "description": [], - "signature": [ - "AuditHttp", - " | undefined" + "{ space_id?: string | undefined; session_id?: string | undefined; saved_object?: { type: string; id: string; } | undefined; authentication_provider?: string | undefined; authentication_type?: string | undefined; authentication_realm?: string | undefined; lookup_realm?: string | undefined; add_to_spaces?: readonly string[] | undefined; delete_from_spaces?: readonly string[] | undefined; } | undefined" ], "path": "x-pack/plugins/security/server/audit/audit_events.ts", "deprecated": false, diff --git a/api_docs/security.mdx b/api_docs/security.mdx index 900aa3bb450d5..5dd817f4385dd 100644 --- a/api_docs/security.mdx +++ b/api_docs/security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/security title: "security" image: https://source.unsplash.com/400x175/?github description: API docs for the security plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'security'] --- import securityObj from './security.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Platform Security](https://github.com/orgs/elastic/teams/kibana-securit | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 252 | 0 | 92 | 3 | +| 251 | 0 | 91 | 1 | ## Client diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index 99926bcde22d8..866b879c3bda2 100644 --- a/api_docs/security_solution.mdx +++ b/api_docs/security_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolution title: "securitySolution" image: https://source.unsplash.com/400x175/?github description: API docs for the securitySolution plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolution'] --- import securitySolutionObj from './security_solution.devdocs.json'; diff --git a/api_docs/session_view.mdx b/api_docs/session_view.mdx index 3aac95659920b..7a167e6fbb733 100644 --- a/api_docs/session_view.mdx +++ b/api_docs/session_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/sessionView title: "sessionView" image: https://source.unsplash.com/400x175/?github description: API docs for the sessionView plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'sessionView'] --- import sessionViewObj from './session_view.devdocs.json'; diff --git a/api_docs/share.mdx b/api_docs/share.mdx index cd88fcb854d3b..37032e84f4f1f 100644 --- a/api_docs/share.mdx +++ b/api_docs/share.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/share title: "share" image: https://source.unsplash.com/400x175/?github description: API docs for the share plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'share'] --- import shareObj from './share.devdocs.json'; diff --git a/api_docs/snapshot_restore.mdx b/api_docs/snapshot_restore.mdx index 05d9964c0e181..178f32d22252f 100644 --- a/api_docs/snapshot_restore.mdx +++ b/api_docs/snapshot_restore.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/snapshotRestore title: "snapshotRestore" image: https://source.unsplash.com/400x175/?github description: API docs for the snapshotRestore plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'snapshotRestore'] --- import snapshotRestoreObj from './snapshot_restore.devdocs.json'; diff --git a/api_docs/spaces.mdx b/api_docs/spaces.mdx index 59c7ba655bcee..501f66bd0fe1c 100644 --- a/api_docs/spaces.mdx +++ b/api_docs/spaces.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/spaces title: "spaces" image: https://source.unsplash.com/400x175/?github description: API docs for the spaces plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'spaces'] --- import spacesObj from './spaces.devdocs.json'; diff --git a/api_docs/stack_alerts.mdx b/api_docs/stack_alerts.mdx index 03fd5505fc588..42e31c0400f9d 100644 --- a/api_docs/stack_alerts.mdx +++ b/api_docs/stack_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackAlerts title: "stackAlerts" image: https://source.unsplash.com/400x175/?github description: API docs for the stackAlerts plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackAlerts'] --- import stackAlertsObj from './stack_alerts.devdocs.json'; diff --git a/api_docs/stack_connectors.mdx b/api_docs/stack_connectors.mdx index 2a033b721d672..e898dc562de74 100644 --- a/api_docs/stack_connectors.mdx +++ b/api_docs/stack_connectors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackConnectors title: "stackConnectors" image: https://source.unsplash.com/400x175/?github description: API docs for the stackConnectors plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackConnectors'] --- import stackConnectorsObj from './stack_connectors.devdocs.json'; diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx index 2d590c0a4ba92..2b9ca4d707e47 100644 --- a/api_docs/task_manager.mdx +++ b/api_docs/task_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/taskManager title: "taskManager" image: https://source.unsplash.com/400x175/?github description: API docs for the taskManager plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'taskManager'] --- import taskManagerObj from './task_manager.devdocs.json'; diff --git a/api_docs/telemetry.mdx b/api_docs/telemetry.mdx index 1f7740e37be17..7f43fa3b93017 100644 --- a/api_docs/telemetry.mdx +++ b/api_docs/telemetry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetry title: "telemetry" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetry plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetry'] --- import telemetryObj from './telemetry.devdocs.json'; diff --git a/api_docs/telemetry_collection_manager.mdx b/api_docs/telemetry_collection_manager.mdx index 1c0a5615a8d91..a4c6dd117ca4b 100644 --- a/api_docs/telemetry_collection_manager.mdx +++ b/api_docs/telemetry_collection_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionManager title: "telemetryCollectionManager" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionManager plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionManager'] --- import telemetryCollectionManagerObj from './telemetry_collection_manager.devdocs.json'; diff --git a/api_docs/telemetry_collection_xpack.mdx b/api_docs/telemetry_collection_xpack.mdx index 70dc95ca7d411..6ce46d35ecc10 100644 --- a/api_docs/telemetry_collection_xpack.mdx +++ b/api_docs/telemetry_collection_xpack.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionXpack title: "telemetryCollectionXpack" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionXpack plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionXpack'] --- import telemetryCollectionXpackObj from './telemetry_collection_xpack.devdocs.json'; diff --git a/api_docs/telemetry_management_section.mdx b/api_docs/telemetry_management_section.mdx index c49d62848ffa5..7350268ed6db9 100644 --- a/api_docs/telemetry_management_section.mdx +++ b/api_docs/telemetry_management_section.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryManagementSection title: "telemetryManagementSection" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryManagementSection plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryManagementSection'] --- import telemetryManagementSectionObj from './telemetry_management_section.devdocs.json'; diff --git a/api_docs/threat_intelligence.mdx b/api_docs/threat_intelligence.mdx index 473411a028530..331bc29887922 100644 --- a/api_docs/threat_intelligence.mdx +++ b/api_docs/threat_intelligence.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/threatIntelligence title: "threatIntelligence" image: https://source.unsplash.com/400x175/?github description: API docs for the threatIntelligence plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'threatIntelligence'] --- import threatIntelligenceObj from './threat_intelligence.devdocs.json'; diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index 6166500e90c57..df543dc07f8e1 100644 --- a/api_docs/timelines.mdx +++ b/api_docs/timelines.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/timelines title: "timelines" image: https://source.unsplash.com/400x175/?github description: API docs for the timelines plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'timelines'] --- import timelinesObj from './timelines.devdocs.json'; diff --git a/api_docs/transform.mdx b/api_docs/transform.mdx index 4d74268c0ab55..8188107e7f902 100644 --- a/api_docs/transform.mdx +++ b/api_docs/transform.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/transform title: "transform" image: https://source.unsplash.com/400x175/?github description: API docs for the transform plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] --- import transformObj from './transform.devdocs.json'; diff --git a/api_docs/triggers_actions_ui.devdocs.json b/api_docs/triggers_actions_ui.devdocs.json index 1037c842fe824..6d7511a5af97d 100644 --- a/api_docs/triggers_actions_ui.devdocs.json +++ b/api_docs/triggers_actions_ui.devdocs.json @@ -327,6 +327,61 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.bulkDeleteRules", + "type": "Function", + "tags": [], + "label": "bulkDeleteRules", + "description": [], + "signature": [ + "({ filter, ids, http, }: ", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.BulkOperationAttributes", + "text": "BulkOperationAttributes" + }, + ") => Promise<", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.BulkOperationResponse", + "text": "BulkOperationResponse" + }, + ">" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_delete.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.bulkDeleteRules.$1", + "type": "CompoundType", + "tags": [], + "label": "{\n filter,\n ids,\n http,\n}", + "description": [], + "signature": [ + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.BulkOperationAttributes", + "text": "BulkOperationAttributes" + } + ], + "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/bulk_delete.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.ButtonGroupField", @@ -393,79 +448,6 @@ ], "initialIsOpen": false }, - { - "parentPluginId": "triggersActionsUi", - "id": "def-public.deleteRules", - "type": "Function", - "tags": [], - "label": "deleteRules", - "description": [], - "signature": [ - "({\n ids,\n http,\n}: { ids: string[]; http: ", - { - "pluginId": "@kbn/core-http-browser", - "scope": "common", - "docId": "kibKbnCoreHttpBrowserPluginApi", - "section": "def-common.HttpSetup", - "text": "HttpSetup" - }, - "; }) => Promise<{ successes: string[]; errors: string[]; }>" - ], - "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "triggersActionsUi", - "id": "def-public.deleteRules.$1", - "type": "Object", - "tags": [], - "label": "{\n ids,\n http,\n}", - "description": [], - "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "triggersActionsUi", - "id": "def-public.deleteRules.$1.ids", - "type": "Array", - "tags": [], - "label": "ids", - "description": [], - "signature": [ - "string[]" - ], - "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "triggersActionsUi", - "id": "def-public.deleteRules.$1.http", - "type": "Object", - "tags": [], - "label": "http", - "description": [], - "signature": [ - { - "pluginId": "@kbn/core-http-browser", - "scope": "common", - "docId": "kibKbnCoreHttpBrowserPluginApi", - "section": "def-common.HttpSetup", - "text": "HttpSetup" - } - ], - "path": "x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/delete.ts", - "deprecated": false, - "trackAdoption": false - } - ] - } - ], - "returnComment": [], - "initialIsOpen": false - }, { "parentPluginId": "triggersActionsUi", "id": "def-public.EditConnectorFlyout", @@ -4071,6 +4053,81 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.BulkOperationResponse", + "type": "Interface", + "tags": [], + "label": "BulkOperationResponse", + "description": [], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.BulkOperationResponse.rules", + "type": "Array", + "tags": [], + "label": "rules", + "description": [], + "signature": [ + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.Rule", + "text": "Rule" + }, + "<", + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.RuleTypeParams", + "text": "RuleTypeParams" + }, + ">[]" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.BulkOperationResponse.errors", + "type": "Array", + "tags": [], + "label": "errors", + "description": [], + "signature": [ + { + "pluginId": "alerting", + "scope": "server", + "docId": "kibAlertingPluginApi", + "section": "def-server.BulkOperationError", + "text": "BulkOperationError" + }, + "[]" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.BulkOperationResponse.total", + "type": "number", + "tags": [], + "label": "total", + "description": [], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.Comparator", @@ -6468,6 +6525,30 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.BulkOperationAttributes", + "type": "Type", + "tags": [], + "label": "BulkOperationAttributes", + "description": [], + "signature": [ + "BulkOperationAttributesWithoutHttp", + " & { http: ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + }, + "; }" + ], + "path": "x-pack/plugins/triggers_actions_ui/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "triggersActionsUi", "id": "def-public.connectorDeprecatedMessage", diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index 8dec5a9d844e7..2c8efbb789da1 100644 --- a/api_docs/triggers_actions_ui.mdx +++ b/api_docs/triggers_actions_ui.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/triggersActionsUi title: "triggersActionsUi" image: https://source.unsplash.com/400x175/?github description: API docs for the triggersActionsUi plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'triggersActionsUi'] --- import triggersActionsUiObj from './triggers_actions_ui.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Response Ops](https://github.com/orgs/elastic/teams/response-ops) for q | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 576 | 11 | 547 | 52 | +| 579 | 11 | 550 | 53 | ## Client diff --git a/api_docs/ui_actions.mdx b/api_docs/ui_actions.mdx index 7d7eb6ef75c0d..bc8ccf9a6fd85 100644 --- a/api_docs/ui_actions.mdx +++ b/api_docs/ui_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActions title: "uiActions" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActions plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActions'] --- import uiActionsObj from './ui_actions.devdocs.json'; diff --git a/api_docs/ui_actions_enhanced.mdx b/api_docs/ui_actions_enhanced.mdx index 2a5aa518fde34..416c8e0b17d57 100644 --- a/api_docs/ui_actions_enhanced.mdx +++ b/api_docs/ui_actions_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActionsEnhanced title: "uiActionsEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActionsEnhanced plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActionsEnhanced'] --- import uiActionsEnhancedObj from './ui_actions_enhanced.devdocs.json'; diff --git a/api_docs/unified_field_list.mdx b/api_docs/unified_field_list.mdx index 0cee7f7bb2a36..7e903c8838874 100644 --- a/api_docs/unified_field_list.mdx +++ b/api_docs/unified_field_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedFieldList title: "unifiedFieldList" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedFieldList plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedFieldList'] --- import unifiedFieldListObj from './unified_field_list.devdocs.json'; diff --git a/api_docs/unified_histogram.mdx b/api_docs/unified_histogram.mdx index cb62fbab2b650..57d49162b8dc2 100644 --- a/api_docs/unified_histogram.mdx +++ b/api_docs/unified_histogram.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedHistogram title: "unifiedHistogram" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedHistogram plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedHistogram'] --- import unifiedHistogramObj from './unified_histogram.devdocs.json'; diff --git a/api_docs/unified_search.devdocs.json b/api_docs/unified_search.devdocs.json index 72b23fec6e3e6..d8ea7d3f81b71 100644 --- a/api_docs/unified_search.devdocs.json +++ b/api_docs/unified_search.devdocs.json @@ -106,6 +106,42 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "unifiedSearch", + "id": "def-public.FilterBadgeGroup", + "type": "Function", + "tags": [], + "label": "FilterBadgeGroup", + "description": [ + "\nA `FilterBadgeGroup` component that is wrapped by the `withSuspense` HOC. This component can\nbe used directly by consumers and will load the `FilterBadgeGroupLazy` component lazily with\na predefined fallback and error boundary." + ], + "signature": [ + "React.ForwardRefExoticComponent<", + "FilterBadgeGroupProps", + " & React.RefAttributes<{}>>" + ], + "path": "src/plugins/unified_search/public/filter_badge/index.ts", + "deprecated": false, + "trackAdoption": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "unifiedSearch", + "id": "def-public.FilterBadgeGroup.$1", + "type": "Uncategorized", + "tags": [], + "label": "props", + "description": [], + "signature": [ + "P" + ], + "path": "node_modules/@types/react/index.d.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "unifiedSearch", "id": "def-public.FilterItem", @@ -210,43 +246,6 @@ "returnComment": [], "initialIsOpen": false }, - { - "parentPluginId": "unifiedSearch", - "id": "def-public.FilterLabel", - "type": "Function", - "tags": [], - "label": "FilterLabel", - "description": [ - "\nRenders the label for a single filter pill" - ], - "signature": [ - "(props: ", - "FilterLabelProps", - ") => JSX.Element" - ], - "path": "src/plugins/unified_search/public/filter_bar/index.tsx", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "unifiedSearch", - "id": "def-public.FilterLabel.$1", - "type": "Object", - "tags": [], - "label": "props", - "description": [], - "signature": [ - "FilterLabelProps" - ], - "path": "src/plugins/unified_search/public/filter_bar/index.tsx", - "deprecated": false, - "trackAdoption": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, { "parentPluginId": "unifiedSearch", "id": "def-public.QueryStringInput", diff --git a/api_docs/unified_search.mdx b/api_docs/unified_search.mdx index f7a1b2328fc4a..fec2df71d2b0a 100644 --- a/api_docs/unified_search.mdx +++ b/api_docs/unified_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch title: "unifiedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch'] --- import unifiedSearchObj from './unified_search.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Visualizations](https://github.com/orgs/elastic/teams/kibana-visualizat | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 134 | 2 | 106 | 18 | +| 134 | 2 | 105 | 18 | ## Client diff --git a/api_docs/unified_search_autocomplete.mdx b/api_docs/unified_search_autocomplete.mdx index ebe398f5ac390..5c5f8e68d808a 100644 --- a/api_docs/unified_search_autocomplete.mdx +++ b/api_docs/unified_search_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch-autocomplete title: "unifiedSearch.autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch.autocomplete plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch.autocomplete'] --- import unifiedSearchAutocompleteObj from './unified_search_autocomplete.devdocs.json'; @@ -21,7 +21,7 @@ Contact [Visualizations](https://github.com/orgs/elastic/teams/kibana-visualizat | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 134 | 2 | 106 | 18 | +| 134 | 2 | 105 | 18 | ## Client diff --git a/api_docs/url_forwarding.mdx b/api_docs/url_forwarding.mdx index b16087a30a7db..aa2554a193d66 100644 --- a/api_docs/url_forwarding.mdx +++ b/api_docs/url_forwarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/urlForwarding title: "urlForwarding" image: https://source.unsplash.com/400x175/?github description: API docs for the urlForwarding plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'urlForwarding'] --- import urlForwardingObj from './url_forwarding.devdocs.json'; diff --git a/api_docs/usage_collection.mdx b/api_docs/usage_collection.mdx index deeb87ad48480..d6ae81817b570 100644 --- a/api_docs/usage_collection.mdx +++ b/api_docs/usage_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/usageCollection title: "usageCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the usageCollection plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'usageCollection'] --- import usageCollectionObj from './usage_collection.devdocs.json'; diff --git a/api_docs/ux.mdx b/api_docs/ux.mdx index a16155636097b..3830ed5b9d61b 100644 --- a/api_docs/ux.mdx +++ b/api_docs/ux.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ux title: "ux" image: https://source.unsplash.com/400x175/?github description: API docs for the ux plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ux'] --- import uxObj from './ux.devdocs.json'; diff --git a/api_docs/vis_default_editor.mdx b/api_docs/vis_default_editor.mdx index 8895e5c38ccc5..42c3043a43eea 100644 --- a/api_docs/vis_default_editor.mdx +++ b/api_docs/vis_default_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visDefaultEditor title: "visDefaultEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the visDefaultEditor plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visDefaultEditor'] --- import visDefaultEditorObj from './vis_default_editor.devdocs.json'; diff --git a/api_docs/vis_type_gauge.mdx b/api_docs/vis_type_gauge.mdx index a0e2c0c17307f..b810edf7210b2 100644 --- a/api_docs/vis_type_gauge.mdx +++ b/api_docs/vis_type_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeGauge title: "visTypeGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeGauge plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeGauge'] --- import visTypeGaugeObj from './vis_type_gauge.devdocs.json'; diff --git a/api_docs/vis_type_heatmap.mdx b/api_docs/vis_type_heatmap.mdx index 529bc498e0ffb..74f439be1870f 100644 --- a/api_docs/vis_type_heatmap.mdx +++ b/api_docs/vis_type_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeHeatmap title: "visTypeHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeHeatmap plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeHeatmap'] --- import visTypeHeatmapObj from './vis_type_heatmap.devdocs.json'; diff --git a/api_docs/vis_type_pie.mdx b/api_docs/vis_type_pie.mdx index 3f33bdc5e9d85..1c6134bf5ccf4 100644 --- a/api_docs/vis_type_pie.mdx +++ b/api_docs/vis_type_pie.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypePie title: "visTypePie" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypePie plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypePie'] --- import visTypePieObj from './vis_type_pie.devdocs.json'; diff --git a/api_docs/vis_type_table.mdx b/api_docs/vis_type_table.mdx index c10c580ac1648..61cf638def8f3 100644 --- a/api_docs/vis_type_table.mdx +++ b/api_docs/vis_type_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTable title: "visTypeTable" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTable plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTable'] --- import visTypeTableObj from './vis_type_table.devdocs.json'; diff --git a/api_docs/vis_type_timelion.mdx b/api_docs/vis_type_timelion.mdx index 325958d850bfd..85deca52ccc01 100644 --- a/api_docs/vis_type_timelion.mdx +++ b/api_docs/vis_type_timelion.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimelion title: "visTypeTimelion" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimelion plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimelion'] --- import visTypeTimelionObj from './vis_type_timelion.devdocs.json'; diff --git a/api_docs/vis_type_timeseries.mdx b/api_docs/vis_type_timeseries.mdx index 1291dde565226..5a623f8262433 100644 --- a/api_docs/vis_type_timeseries.mdx +++ b/api_docs/vis_type_timeseries.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimeseries title: "visTypeTimeseries" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimeseries plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimeseries'] --- import visTypeTimeseriesObj from './vis_type_timeseries.devdocs.json'; diff --git a/api_docs/vis_type_vega.mdx b/api_docs/vis_type_vega.mdx index 3538a2585572e..20e9dd54422bd 100644 --- a/api_docs/vis_type_vega.mdx +++ b/api_docs/vis_type_vega.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVega title: "visTypeVega" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVega plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVega'] --- import visTypeVegaObj from './vis_type_vega.devdocs.json'; diff --git a/api_docs/vis_type_vislib.mdx b/api_docs/vis_type_vislib.mdx index 4d1c2a470d619..ca00e0e714133 100644 --- a/api_docs/vis_type_vislib.mdx +++ b/api_docs/vis_type_vislib.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVislib title: "visTypeVislib" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVislib plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVislib'] --- import visTypeVislibObj from './vis_type_vislib.devdocs.json'; diff --git a/api_docs/vis_type_xy.mdx b/api_docs/vis_type_xy.mdx index 12e8c56b1a2b8..e839a7bb77f04 100644 --- a/api_docs/vis_type_xy.mdx +++ b/api_docs/vis_type_xy.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeXy title: "visTypeXy" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeXy plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeXy'] --- import visTypeXyObj from './vis_type_xy.devdocs.json'; diff --git a/api_docs/visualizations.mdx b/api_docs/visualizations.mdx index 60cb4e713e317..991f33de129d2 100644 --- a/api_docs/visualizations.mdx +++ b/api_docs/visualizations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visualizations title: "visualizations" image: https://source.unsplash.com/400x175/?github description: API docs for the visualizations plugin -date: 2022-12-19 +date: 2022-12-20 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] --- import visualizationsObj from './visualizations.devdocs.json'; From 141078e8f5f970584243a78a93887adb8c88d4b2 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Tue, 20 Dec 2022 07:31:23 +0100 Subject: [PATCH 36/55] [Security Solution] Add a rule management filters internal endpoint (#146826) **Addresses:** https://github.com/elastic/kibana/issues/137428 ## Summary Adds a new internal lightweight endpoint to fetch rules related information like the number of installed prebuilt rules, the number of custom rules and etc. ## Details This PR adds a quite simple and lightweight endpoint for fetching rules related information which is - the number of installed prebuilt rules - the number of custom rules - tags UI has been updated accordingly. The result of the endpoint are mostly used in the rules table filter but not limited to. **_The added endpoint doesn't implement full aggregation for fetching rule numbers so it's planned to be done in the following PR._** ### Comparison The following screenshots from the browser's network tab demonstrate that the new endpoint is faster which is good since it's intended to be updated executed relatively often whenever the rules are updated. Prebuilt rules endpoint which was used for fetching rules related information Screenshot 2022-12-04 at 21 50 50 The new endpoint ![image](https://user-images.githubusercontent.com/3775283/205887909-39b1f18d-5181-4a13-b16c-0291080011da.png) ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../alerting/server/rules_client.mock.ts | 2 +- .../server/rules_client/methods/aggregate.ts | 7 +- .../rules_client/tests/aggregate.test.ts | 34 ++++ .../api/rules/filters/response_schema.test.ts | 151 ++++++++++++++ .../api/rules/filters/response_schema.ts | 22 +++ .../rule_management/api/urls.ts | 10 + .../rule_management/api/api.test.ts | 21 -- .../rule_management/api/api.ts | 16 +- .../api/hooks/use_bulk_action_mutation.ts | 12 +- .../use_create_prebuilt_rules_mutation.ts | 8 +- .../api/hooks/use_create_rule_mutation.ts | 9 +- ...use_fetch_rule_management_filters_query.ts | 48 +++++ .../api/hooks/use_fetch_tags_query.ts | 43 ---- .../api/hooks/use_update_rule_mutation.ts | 8 +- .../rule_management/logic/translations.ts | 14 +- ...tags.ts => use_rule_management_filters.ts} | 13 +- .../bulk_actions/forms/tags_form.tsx | 9 +- .../rules_table_filters.tsx | 15 +- .../components/rules_table/rules_tables.tsx | 12 +- .../pages/rule_management/index.tsx | 8 +- .../routes/__mocks__/request_responses.ts | 8 + .../rule_management/api/register_routes.ts | 4 + .../api/rules/filters/route.test.ts | 79 ++++++++ .../api/rules/filters/route.ts | 96 +++++++++ .../api/tags/read_tags/read_tags.test.ts | 185 +----------------- .../api/tags/read_tags/read_tags.ts | 62 ++---- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../spaces_only/tests/alerting/aggregate.ts | 29 +++ .../group1/get_rule_management_filters.ts | 113 +++++++++++ .../security_and_spaces/group1/index.ts | 1 + .../create_prebuilt_rule_saved_objects.ts | 3 + 33 files changed, 681 insertions(+), 364 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/filters/response_schema.test.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/filters/response_schema.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/rule_management/api/urls.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rule_management_filters_query.ts delete mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_tags_query.ts rename x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/{use_tags.ts => use_rule_management_filters.ts} (61%) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/filters/route.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/filters/route.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_rule_management_filters.ts diff --git a/x-pack/plugins/alerting/server/rules_client.mock.ts b/x-pack/plugins/alerting/server/rules_client.mock.ts index cef3381600fa1..585d3b1c6aa05 100644 --- a/x-pack/plugins/alerting/server/rules_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_client.mock.ts @@ -12,7 +12,7 @@ export type RulesClientMock = jest.Mocked; const createRulesClientMock = () => { const mocked: RulesClientMock = { - aggregate: jest.fn(), + aggregate: jest.fn().mockReturnValue({ alertExecutionStatus: {}, ruleLastRunOutcome: {} }), create: jest.fn(), get: jest.fn(), resolve: jest.fn(), diff --git a/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts b/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts index 79a07b3ebad49..2c9b991618aa1 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/aggregate.ts @@ -22,6 +22,7 @@ export interface AggregateOptions extends IndexType { id: string; }; filter?: string | KueryNode; + maxTags?: number; } interface IndexType { @@ -79,7 +80,9 @@ export interface RuleAggregation { export async function aggregate( context: RulesClientContext, - { options: { fields, filter, ...options } = {} }: { options?: AggregateOptions } = {} + { + options: { fields, filter, maxTags = 50, ...options } = {}, + }: { options?: AggregateOptions } = {} ): Promise { let authorizationTuple; try { @@ -123,7 +126,7 @@ export async function aggregate( terms: { field: 'alert.attributes.muteAll' }, }, tags: { - terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: 50 }, + terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: maxTags }, }, snoozed: { nested: { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts index 937c027ed04ce..7a9e7db434818 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts @@ -310,4 +310,38 @@ describe('aggregate()', () => { }) ); }); + + describe('tags number limit', () => { + test('sets to default (50) if it is not provided', async () => { + const rulesClient = new RulesClient(rulesClientParams); + + await rulesClient.aggregate(); + + expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchObject([ + { + aggs: { + tags: { + terms: { size: 50 }, + }, + }, + }, + ]); + }); + + test('sets to the provided value', async () => { + const rulesClient = new RulesClient(rulesClientParams); + + await rulesClient.aggregate({ options: { maxTags: 1000 } }); + + expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchObject([ + { + aggs: { + tags: { + terms: { size: 1000 }, + }, + }, + }, + ]); + }); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/filters/response_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/filters/response_schema.test.ts new file mode 100644 index 0000000000000..85a28a3042092 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/filters/response_schema.test.ts @@ -0,0 +1,151 @@ +/* + * 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 { left } from 'fp-ts/lib/Either'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +import { RuleManagementFiltersResponse } from './response_schema'; + +describe('Rule management filters response schema', () => { + test('it should validate an empty response with defaults', () => { + const payload: RuleManagementFiltersResponse = { + rules_summary: { + custom_count: 0, + prebuilt_installed_count: 0, + }, + aggregated_fields: { + tags: [], + }, + }; + const decoded = RuleManagementFiltersResponse.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an non empty response with defaults', () => { + const payload: RuleManagementFiltersResponse = { + rules_summary: { + custom_count: 10, + prebuilt_installed_count: 20, + }, + aggregated_fields: { + tags: ['a', 'b', 'c'], + }, + }; + const decoded = RuleManagementFiltersResponse.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an extra invalid field added', () => { + const payload: RuleManagementFiltersResponse & { invalid_field: string } = { + rules_summary: { + custom_count: 0, + prebuilt_installed_count: 0, + }, + aggregated_fields: { + tags: [], + }, + invalid_field: 'invalid', + }; + const decoded = RuleManagementFiltersResponse.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_field"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an empty response with a negative "summary.prebuilt_installed_count" number', () => { + const payload: RuleManagementFiltersResponse = { + rules_summary: { + custom_count: 0, + prebuilt_installed_count: -1, + }, + aggregated_fields: { + tags: [], + }, + }; + const decoded = RuleManagementFiltersResponse.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "rules_summary,prebuilt_installed_count"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an empty response with a negative "summary.custom_count"', () => { + const payload: RuleManagementFiltersResponse = { + rules_summary: { + custom_count: -1, + prebuilt_installed_count: 0, + }, + aggregated_fields: { + tags: [], + }, + }; + const decoded = RuleManagementFiltersResponse.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "rules_summary,custom_count"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an empty prepackaged response if "summary.prebuilt_installed_count" is not there', () => { + const payload: RuleManagementFiltersResponse = { + rules_summary: { + custom_count: 0, + prebuilt_installed_count: 0, + }, + aggregated_fields: { + tags: [], + }, + }; + // @ts-expect-error + delete payload.rules_summary.prebuilt_installed_count; + const decoded = RuleManagementFiltersResponse.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "rules_summary,prebuilt_installed_count"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an empty response with wrong "aggregated_fields.tags"', () => { + const payload: RuleManagementFiltersResponse = { + rules_summary: { + custom_count: 0, + prebuilt_installed_count: 0, + }, + aggregated_fields: { + // @ts-expect-error Passing an invalid value for the test + tags: [1], + }, + }; + const decoded = RuleManagementFiltersResponse.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "aggregated_fields,tags"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/filters/response_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/filters/response_schema.ts new file mode 100644 index 0000000000000..89d12bff729b3 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/filters/response_schema.ts @@ -0,0 +1,22 @@ +/* + * 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 * as t from 'io-ts'; +import { PositiveInteger } from '@kbn/securitysolution-io-ts-types'; + +export type RuleManagementFiltersResponse = t.TypeOf; +export const RuleManagementFiltersResponse = t.exact( + t.type({ + rules_summary: t.type({ + custom_count: PositiveInteger, + prebuilt_installed_count: PositiveInteger, + }), + aggregated_fields: t.type({ + tags: t.array(t.string), + }), + }) +); diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/urls.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/urls.ts new file mode 100644 index 0000000000000..4a4cae72cf04d --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/urls.ts @@ -0,0 +1,10 @@ +/* + * 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 { INTERNAL_DETECTION_ENGINE_URL } from '../../../constants'; + +export const RULE_MANAGEMENT_FILTERS_URL = `${INTERNAL_DETECTION_ENGINE_URL}/rules/_rule_management_filters`; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts index 265cf9d42e9d2..bf1ac32d2bc78 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts @@ -31,7 +31,6 @@ import { createPrepackagedRules, importRules, exportRules, - fetchTags, getPrePackagedRulesStatus, previewRule, findRuleExceptionReferences, @@ -630,26 +629,6 @@ describe('Detections Rules API', () => { }); }); - describe('fetchTags', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(['some', 'tags']); - }); - - test('check parameter url when fetching tags', async () => { - await fetchTags({ signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/tags', { - signal: abortCtrl.signal, - method: 'GET', - }); - }); - - test('happy path', async () => { - const resp = await fetchTags({ signal: abortCtrl.signal }); - expect(resp).toEqual(['some', 'tags']); - }); - }); - describe('getPrePackagedRulesStatus', () => { const prePackagedRulesStatus = { rules_custom_installed: 33, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts index e7fe4dd64fb7d..9e4f22484a00d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts @@ -11,13 +11,14 @@ import type { ExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import type { RuleManagementFiltersResponse } from '../../../../common/detection_engine/rule_management/api/rules/filters/response_schema'; +import { RULE_MANAGEMENT_FILTERS_URL } from '../../../../common/detection_engine/rule_management/api/urls'; import type { BulkActionsDryRunErrCode } from '../../../../common/constants'; import { DETECTION_ENGINE_RULES_BULK_ACTION, DETECTION_ENGINE_RULES_PREVIEW, DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_RULES_URL_FIND, - DETECTION_ENGINE_TAGS_URL, } from '../../../../common/constants'; import { @@ -33,7 +34,6 @@ import type { BulkActionDuplicatePayload, } from '../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; import { BulkActionType } from '../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; - import type { RuleResponse, PreviewResponse, @@ -388,17 +388,19 @@ export const exportRules = async ({ }); }; -export type FetchTagsResponse = string[]; - /** - * Fetch all unique Tags used by Rules + * Fetch rule filters related information like installed rules count, tags and etc * * @param signal to cancel request * * @throws An error if response is not OK */ -export const fetchTags = async ({ signal }: { signal?: AbortSignal }): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_TAGS_URL, { +export const fetchRuleManagementFilters = async ({ + signal, +}: { + signal?: AbortSignal; +}): Promise => + KibanaServices.get().http.fetch(RULE_MANAGEMENT_FILTERS_URL, { method: 'GET', signal, }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_action_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_action_mutation.ts index 3f652063707c2..2647dfae6934e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_action_mutation.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_bulk_action_mutation.ts @@ -10,11 +10,11 @@ import type { IHttpFetchError } from '@kbn/core/public'; import { BulkActionType } from '../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; import type { BulkActionErrorResponse, BulkActionResponse, PerformBulkActionProps } from '../api'; import { performBulkAction } from '../api'; +import { DETECTION_ENGINE_RULES_BULK_ACTION } from '../../../../../common/constants'; import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; import { useInvalidateFindRulesQuery, useUpdateRulesCache } from './use_find_rules_query'; -import { useInvalidateFetchTagsQuery } from './use_fetch_tags_query'; import { useInvalidateFetchRuleByIdQuery } from './use_fetch_rule_by_id_query'; -import { DETECTION_ENGINE_RULES_BULK_ACTION } from '../../../../../common/constants'; +import { useInvalidateFetchRuleManagementFiltersQuery } from './use_fetch_rule_management_filters_query'; export const BULK_ACTION_MUTATION_KEY = ['POST', DETECTION_ENGINE_RULES_BULK_ACTION]; @@ -27,7 +27,7 @@ export const useBulkActionMutation = ( ) => { const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); const invalidateFetchRuleByIdQuery = useInvalidateFetchRuleByIdQuery(); - const invalidateFetchTagsQuery = useInvalidateFetchTagsQuery(); + const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery(); const invalidateFetchPrebuiltRulesStatusQuery = useInvalidateFetchPrebuiltRulesStatusQuery(); const updateRulesCache = useUpdateRulesCache(); @@ -66,12 +66,12 @@ export const useBulkActionMutation = ( case BulkActionType.delete: invalidateFindRulesQuery(); invalidateFetchRuleByIdQuery(); - invalidateFetchTagsQuery(); + invalidateFetchRuleManagementFilters(); invalidateFetchPrebuiltRulesStatusQuery(); break; case BulkActionType.duplicate: invalidateFindRulesQuery(); - invalidateFetchPrebuiltRulesStatusQuery(); + invalidateFetchRuleManagementFilters(); break; case BulkActionType.edit: if (updatedRules) { @@ -82,7 +82,7 @@ export const useBulkActionMutation = ( invalidateFindRulesQuery(); } invalidateFetchRuleByIdQuery(); - invalidateFetchTagsQuery(); + invalidateFetchRuleManagementFilters(); break; } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_create_prebuilt_rules_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_create_prebuilt_rules_mutation.ts index a88f011ae8ea4..41bce8f0cc154 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_create_prebuilt_rules_mutation.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_create_prebuilt_rules_mutation.ts @@ -6,12 +6,12 @@ */ import type { UseMutationOptions } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query'; +import { PREBUILT_RULES_URL } from '../../../../../common/detection_engine/prebuilt_rules/api/urls'; import type { CreatePrepackagedRulesResponse } from '../api'; import { createPrepackagedRules } from '../api'; import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; import { useInvalidateFindRulesQuery } from './use_find_rules_query'; -import { useInvalidateFetchTagsQuery } from './use_fetch_tags_query'; -import { PREBUILT_RULES_URL } from '../../../../../common/detection_engine/prebuilt_rules/api/urls'; +import { useInvalidateFetchRuleManagementFiltersQuery } from './use_fetch_rule_management_filters_query'; export const CREATE_PREBUILT_RULES_MUTATION_KEY = ['PUT', PREBUILT_RULES_URL]; @@ -20,7 +20,7 @@ export const useCreatePrebuiltRulesMutation = ( ) => { const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); const invalidatePrePackagedRulesStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); - const invalidateFetchTagsQuery = useInvalidateFetchTagsQuery(); + const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery(); return useMutation(() => createPrepackagedRules(), { ...options, @@ -30,7 +30,7 @@ export const useCreatePrebuiltRulesMutation = ( // the number of rules might change after the installation invalidatePrePackagedRulesStatus(); invalidateFindRulesQuery(); - invalidateFetchTagsQuery(); + invalidateFetchRuleManagementFilters(); if (options?.onSettled) { options.onSettled(...args); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_create_rule_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_create_rule_mutation.ts index a2901e1ed88bc..6d17505cdac29 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_create_rule_mutation.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_create_rule_mutation.ts @@ -13,8 +13,7 @@ import type { } from '../../../../../common/detection_engine/rule_schema'; import { transformOutput } from '../../../../detections/containers/detection_engine/rules/transforms'; import { createRule } from '../api'; -import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query'; -import { useInvalidateFetchTagsQuery } from './use_fetch_tags_query'; +import { useInvalidateFetchRuleManagementFiltersQuery } from './use_fetch_rule_management_filters_query'; import { useInvalidateFindRulesQuery } from './use_find_rules_query'; export const CREATE_RULE_MUTATION_KEY = ['POST', DETECTION_ENGINE_RULES_URL]; @@ -23,8 +22,7 @@ export const useCreateRuleMutation = ( options?: UseMutationOptions ) => { const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); - const invalidateFetchTagsQuery = useInvalidateFetchTagsQuery(); - const invalidateFetchPrePackagedRulesStatusQuery = useInvalidateFetchPrebuiltRulesStatusQuery(); + const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery(); return useMutation( (rule: RuleCreateProps) => createRule({ rule: transformOutput(rule) }), @@ -32,9 +30,8 @@ export const useCreateRuleMutation = ( ...options, mutationKey: CREATE_RULE_MUTATION_KEY, onSettled: (...args) => { - invalidateFetchPrePackagedRulesStatusQuery(); invalidateFindRulesQuery(); - invalidateFetchTagsQuery(); + invalidateFetchRuleManagementFilters(); if (options?.onSettled) { options.onSettled(...args); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rule_management_filters_query.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rule_management_filters_query.ts new file mode 100644 index 0000000000000..32b2a206970a3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rule_management_filters_query.ts @@ -0,0 +1,48 @@ +/* + * 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 { useCallback } from 'react'; +import type { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { RuleManagementFiltersResponse } from '../../../../../common/detection_engine/rule_management/api/rules/filters/response_schema'; +import { RULE_MANAGEMENT_FILTERS_URL } from '../../../../../common/detection_engine/rule_management/api/urls'; +import { fetchRuleManagementFilters } from '../api'; +import { DEFAULT_QUERY_OPTIONS } from './constants'; + +export const RULE_MANAGEMENT_FILTERS_QUERY_KEY = ['GET', RULE_MANAGEMENT_FILTERS_URL]; + +export const useFetchRuleManagementFiltersQuery = ( + options?: UseQueryOptions +) => { + return useQuery( + RULE_MANAGEMENT_FILTERS_QUERY_KEY, + async ({ signal }) => { + const response = await fetchRuleManagementFilters({ signal }); + return response; + }, + { + ...DEFAULT_QUERY_OPTIONS, + ...options, + } + ); +}; + +/** + * We should use this hook to invalidate the rule management filters cache. For + * example, rule mutations that affect rule set size, like creation or deletion, + * should lead to cache invalidation. + * + * @returns A rules cache invalidation callback + */ +export const useInvalidateFetchRuleManagementFiltersQuery = () => { + const queryClient = useQueryClient(); + + return useCallback(() => { + queryClient.invalidateQueries(RULE_MANAGEMENT_FILTERS_QUERY_KEY, { + refetchType: 'active', + }); + }, [queryClient]); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_tags_query.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_tags_query.ts deleted file mode 100644 index c09ae5d6cb56d..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_tags_query.ts +++ /dev/null @@ -1,43 +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 type { UseQueryOptions } from '@tanstack/react-query'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { useCallback } from 'react'; -import { DETECTION_ENGINE_TAGS_URL } from '../../../../../common/constants'; -import type { FetchTagsResponse } from '../api'; -import { fetchTags } from '../api'; -import { DEFAULT_QUERY_OPTIONS } from './constants'; - -const TAGS_QUERY_KEY = ['GET', DETECTION_ENGINE_TAGS_URL]; - -/** - * Hook for using the list of Tags from the Detection Engine API - * - */ -export const useFetchTagsQuery = (options?: UseQueryOptions) => { - return useQuery( - TAGS_QUERY_KEY, - async ({ signal }) => { - return fetchTags({ signal }); - }, - { - ...DEFAULT_QUERY_OPTIONS, - ...options, - } - ); -}; - -export const useInvalidateFetchTagsQuery = () => { - const queryClient = useQueryClient(); - - return useCallback(() => { - queryClient.invalidateQueries(TAGS_QUERY_KEY, { - refetchType: 'active', - }); - }, [queryClient]); -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_update_rule_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_update_rule_mutation.ts index 13247852521db..21d0c78f49b41 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_update_rule_mutation.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_update_rule_mutation.ts @@ -11,11 +11,11 @@ import type { RuleUpdateProps, } from '../../../../../common/detection_engine/rule_schema'; import { transformOutput } from '../../../../detections/containers/detection_engine/rules/transforms'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { updateRule } from '../api'; import { useInvalidateFindRulesQuery } from './use_find_rules_query'; -import { useInvalidateFetchTagsQuery } from './use_fetch_tags_query'; import { useInvalidateFetchRuleByIdQuery } from './use_fetch_rule_by_id_query'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { useInvalidateFetchRuleManagementFiltersQuery } from './use_fetch_rule_management_filters_query'; export const UPDATE_RULE_MUTATION_KEY = ['PUT', DETECTION_ENGINE_RULES_URL]; @@ -23,7 +23,7 @@ export const useUpdateRuleMutation = ( options?: UseMutationOptions ) => { const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); - const invalidateFetchTagsQuery = useInvalidateFetchTagsQuery(); + const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery(); const invalidateFetchRuleByIdQuery = useInvalidateFetchRuleByIdQuery(); return useMutation( @@ -34,7 +34,7 @@ export const useUpdateRuleMutation = ( onSettled: (...args) => { invalidateFindRulesQuery(); invalidateFetchRuleByIdQuery(); - invalidateFetchTagsQuery(); + invalidateFetchRuleManagementFilters(); if (options?.onSettled) { options.onSettled(...args); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/translations.ts index 36735f1ff11e1..56ebc3e133775 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/translations.ts @@ -14,6 +14,13 @@ export const RULE_AND_TIMELINE_FETCH_FAILURE = i18n.translate( } ); +export const RULE_MANAGEMENT_FILTERS_FETCH_FAILURE = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.ruleManagementFiltersFetchFailure', + { + defaultMessage: 'Failed to fetch rule filters', + } +); + export const RULE_ADD_FAILURE = i18n.translate( 'xpack.securitySolution.containers.detectionEngine.addRuleFailDescription', { @@ -48,10 +55,3 @@ export const TIMELINE_PREPACKAGED_SUCCESS = i18n.translate( defaultMessage: 'Installed pre-packaged timeline templates from elastic', } ); - -export const TAG_FETCH_FAILURE = i18n.translate( - 'xpack.securitySolution.containers.detectionEngine.tagFetchFailDescription', - { - defaultMessage: 'Failed to fetch Tags', - } -); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_tags.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_rule_management_filters.ts similarity index 61% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_tags.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_rule_management_filters.ts index ab3671f262f8a..a16f433b993a1 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_tags.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_rule_management_filters.ts @@ -4,21 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -import { useFetchTagsQuery } from '../api/hooks/use_fetch_tags_query'; +import { useFetchRuleManagementFiltersQuery } from '../api/hooks/use_fetch_rule_management_filters_query'; import * as i18n from './translations'; -/** - * Hook for using the list of Tags from the Detection Engine API - * - */ -export const useTags = () => { +export const useRuleManagementFilters = () => { const { addError } = useAppToasts(); - return useFetchTagsQuery({ + return useFetchRuleManagementFiltersQuery({ onError: (err) => { - addError(err, { title: i18n.TAG_FETCH_FAILURE }); + addError(err, { title: i18n.RULE_MANAGEMENT_FILTERS_FETCH_FAILURE }); }, }); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/tags_form.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/tags_form.tsx index 4288342c03d10..3e09b5dd3bed7 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/tags_form.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/tags_form.tsx @@ -9,6 +9,7 @@ import { EuiCallOut, EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { useMemo } from 'react'; +import { useRuleManagementFilters } from '../../../../../rule_management/logic/use_rule_management_filters'; import type { BulkActionEditPayload } from '../../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; import { BulkActionEditType } from '../../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; import * as i18n from '../../../../../../detections/pages/detection_engine/rules/translations'; @@ -25,7 +26,6 @@ import { } from '../../../../../../shared_imports'; import { BulkEditFormWrapper } from './bulk_edit_form_wrapper'; -import { useTags } from '../../../../../rule_management/logic/use_tags'; type TagsEditActions = | BulkActionEditType.add_tags @@ -78,13 +78,16 @@ interface TagsFormProps { } const TagsFormComponent = ({ editAction, rulesCount, onClose, onConfirm }: TagsFormProps) => { - const { data: tags = [] } = useTags(); + const { data: ruleManagementFilters } = useRuleManagementFilters(); const { form } = useForm({ defaultValue: initialFormData, schema, }); const [{ overwrite }] = useFormData({ form, watch: ['overwrite'] }); - const sortedTags = useMemo(() => caseInsensitiveSort(tags), [tags]); + const sortedTags = useMemo( + () => caseInsensitiveSort(ruleManagementFilters?.aggregated_fields.tags ?? []), + [ruleManagementFilters] + ); const { tagsLabel, tagsHelpText, formTitle } = getFormConfig(editAction); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rules_table_filters.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rules_table_filters.tsx index 0636048384714..2fa5a3806874a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rules_table_filters.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rules_table_filters.tsx @@ -9,13 +9,12 @@ import { EuiFilterButton, EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@ela import { isEqual } from 'lodash/fp'; import React, { useCallback } from 'react'; import styled from 'styled-components'; +import { useRuleManagementFilters } from '../../../../rule_management/logic/use_rule_management_filters'; import { RULES_TABLE_ACTIONS } from '../../../../../common/lib/apm/user_actions'; import { useStartTransaction } from '../../../../../common/lib/apm/use_start_transaction'; -import { usePrePackagedRulesStatus } from '../../../../rule_management/logic/use_pre_packaged_rules_status'; import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations'; import { useRulesTableContext } from '../rules_table/rules_table_context'; import { TagsFilterPopover } from './tags_filter_popover'; -import { useTags } from '../../../../rule_management/logic/use_tags'; import { RuleSearchField } from './rule_search_field'; const FilterWrapper = styled(EuiFlexGroup)` @@ -32,10 +31,10 @@ const RulesTableFiltersComponent = () => { state: { filterOptions }, actions: { setFilterOptions }, } = useRulesTableContext(); - const { data: allTags = [] } = useTags(); - const { data: prePackagedRulesStatus } = usePrePackagedRulesStatus(); - const rulesCustomInstalled = prePackagedRulesStatus?.rules_custom_installed; - const rulesInstalled = prePackagedRulesStatus?.rules_installed; + const { data: ruleManagementFields } = useRuleManagementFilters(); + const allTags = ruleManagementFields?.aggregated_fields.tags ?? []; + const rulesCustomCount = ruleManagementFields?.rules_summary.custom_count; + const rulesPrebuiltInstalledCount = ruleManagementFields?.rules_summary.prebuilt_installed_count; const { showCustomRules, showElasticRules, tags: selectedTags } = filterOptions; @@ -90,7 +89,7 @@ const RulesTableFiltersComponent = () => { withNext > {i18n.ELASTIC_RULES} - {rulesInstalled != null ? ` (${rulesInstalled})` : ''} + {rulesPrebuiltInstalledCount != null ? ` (${rulesPrebuiltInstalledCount ?? ''})` : ''} { data-test-subj="showCustomRulesFilterButton" > {i18n.CUSTOM_RULES} - {rulesCustomInstalled != null ? ` (${rulesCustomInstalled})` : ''} + {rulesCustomCount != null ? ` (${rulesCustomCount})` : ''} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx index 7156126b39c29..95fc837a8e8f5 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_tables.tsx @@ -18,7 +18,6 @@ import { useBoolState } from '../../../../common/hooks/use_bool_state'; import { useValueChanged } from '../../../../common/hooks/use_value_changed'; import { PrePackagedRulesPrompt } from '../../../../detections/components/rules/pre_packaged_rules/load_empty_prompt'; import type { Rule, RulesSortingFields } from '../../../rule_management/logic'; -import { usePrePackagedRulesStatus } from '../../../rule_management/logic/use_pre_packaged_rules_status'; import * as i18n from '../../../../detections/pages/detection_engine/rules/translations'; import type { EuiBasicTableOnChange } from '../../../../detections/pages/detection_engine/rules/types'; import { BulkActionDryRunConfirmation } from './bulk_actions/bulk_action_dry_run_confirmation'; @@ -39,6 +38,7 @@ import { useBulkDuplicateExceptionsConfirmation } from './bulk_actions/use_bulk_ import { BulkActionDuplicateExceptionsConfirmation } from './bulk_actions/bulk_duplicate_exceptions_confirmation'; import { useStartMlJobs } from '../../../rule_management/logic/use_start_ml_jobs'; import { RULES_TABLE_PAGE_SIZE_OPTIONS } from './constants'; +import { useRuleManagementFilters } from '../../../rule_management/logic/use_rule_management_filters'; const INITIAL_SORT_FIELD = 'enabled'; @@ -65,8 +65,7 @@ export const RulesTables = React.memo(({ selectedTab }) => { const tableRef = useRef(null); const rulesTableContext = useRulesTableContext(); - const { data: prePackagedRulesStatus, isLoading: isPrepackagedStatusLoading } = - usePrePackagedRulesStatus(); + const { data: ruleManagementFilters } = useRuleManagementFilters(); const { state: { @@ -214,11 +213,10 @@ export const RulesTables = React.memo(({ selectedTab }) => { }, [rules, isAllSelected, setIsAllSelected, setSelectedRuleIds]); const isTableEmpty = - !isPrepackagedStatusLoading && - prePackagedRulesStatus?.rules_custom_installed === 0 && - prePackagedRulesStatus.rules_installed === 0; + ruleManagementFilters?.rules_summary.custom_count === 0 && + ruleManagementFilters?.rules_summary.prebuilt_installed_count === 0; - const shouldShowRulesTable = !isPrepackagedStatusLoading && !isLoading && !isTableEmpty; + const shouldShowRulesTable = !isLoading && !isTableEmpty; const tableProps = selectedTab === AllRulesTabs.rules diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx index a76c064103cbe..f02075809a46d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx @@ -32,7 +32,6 @@ import { useListsConfig } from '../../../../detections/containers/detection_engi import { redirectToDetections } from '../../../../detections/pages/detection_engine/rules/helpers'; import { useInvalidateFindRulesQuery } from '../../../rule_management/api/hooks/use_find_rules_query'; -import { useInvalidateFetchPrebuiltRulesStatusQuery } from '../../../rule_management/api/hooks/use_fetch_prebuilt_rules_status_query'; import { importRules } from '../../../rule_management/logic'; import { usePrePackagedRulesInstallationStatus } from '../../../rule_management/logic/use_pre_packaged_rules_installation_status'; import { usePrePackagedTimelinesInstallationStatus } from '../../../rule_management/logic/use_pre_packaged_timelines_installation_status'; @@ -42,17 +41,18 @@ import { RulesTableContextProvider } from '../../components/rules_table/rules_ta import * as i18n from '../../../../detections/pages/detection_engine/rules/translations'; import { RulesPageTourComponent } from '../../components/rules_table/alternative_tour/tour'; +import { useInvalidateFetchRuleManagementFiltersQuery } from '../../../rule_management/api/hooks/use_fetch_rule_management_filters_query'; const RulesPageComponent: React.FC = () => { const [isImportModalVisible, showImportModal, hideImportModal] = useBoolState(); const [isValueListFlyoutVisible, showValueListFlyout, hideValueListFlyout] = useBoolState(); const { navigateToApp } = useKibana().services.application; const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); - const invalidateFetchPrebuiltRulesStatusQuery = useInvalidateFetchPrebuiltRulesStatusQuery(); + const invalidateFetchRuleManagementFilters = useInvalidateFetchRuleManagementFiltersQuery(); const invalidateRules = useCallback(() => { invalidateFindRulesQuery(); - invalidateFetchPrebuiltRulesStatusQuery(); - }, [invalidateFindRulesQuery, invalidateFetchPrebuiltRulesStatusQuery]); + invalidateFetchRuleManagementFilters(); + }, [invalidateFindRulesQuery, invalidateFetchRuleManagementFilters]); const [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 7b6c85d486638..8239108728304 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -24,6 +24,7 @@ import { DETECTION_ENGINE_RULES_BULK_CREATE, DETECTION_ENGINE_RULES_URL_FIND, } from '../../../../../common/constants'; +import { RULE_MANAGEMENT_FILTERS_URL } from '../../../../../common/detection_engine/rule_management/api/urls'; import { PREBUILT_RULES_STATUS_URL, @@ -33,6 +34,7 @@ import { getPerformBulkActionSchemaMock, getPerformBulkActionEditSchemaMock, } from '../../../../../common/detection_engine/rule_management/mocks'; + import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/rule_schema/mocks'; import type { QuerySignalsSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema'; import type { SetSignalsStatusSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema'; @@ -189,6 +191,12 @@ export const getPrepackagedRulesStatusRequest = () => path: PREBUILT_RULES_STATUS_URL, }); +export const getRuleManagementFiltersRequest = () => + requestMock.create({ + method: 'get', + path: RULE_MANAGEMENT_FILTERS_URL, + }); + export interface FindHit { page: number; perPage: number; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts index 90a37a7dbe7da..53c4e54484e18 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts @@ -20,6 +20,7 @@ import { deleteRuleRoute } from './rules/delete_rule/route'; import { exportRulesRoute } from './rules/export_rules/route'; import { findRulesRoute } from './rules/find_rules/route'; import { importRulesRoute } from './rules/import_rules/route'; +import { getRuleManagementFilters } from './rules/filters/route'; import { patchRuleRoute } from './rules/patch_rule/route'; import { readRuleRoute } from './rules/read_rule/route'; import { updateRuleRoute } from './rules/update_rule/route'; @@ -56,4 +57,7 @@ export const registerRuleManagementRoutes = ( // Rule tags readTagsRoute(router); + + // Rules filters + getRuleManagementFilters(router); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/filters/route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/filters/route.test.ts new file mode 100644 index 0000000000000..969c8c1480df6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/filters/route.test.ts @@ -0,0 +1,79 @@ +/* + * 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 { getRuleManagementFilters } from './route'; + +import { + getEmptyFindResult, + getFindResultWithSingleHit, + getRuleManagementFiltersRequest, +} from '../../../../routes/__mocks__/request_responses'; +import { requestContextMock, serverMock } from '../../../../routes/__mocks__'; + +describe('Rule management filters route', () => { + let server: ReturnType; + let { clients, context } = requestContextMock.createTools(); + + beforeEach(() => { + jest.clearAllMocks(); + server = serverMock.create(); + ({ clients, context } = requestContextMock.createTools()); + + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); + + getRuleManagementFilters(server.router); + }); + + describe('status codes', () => { + test('returns 200', async () => { + const response = await server.inject( + getRuleManagementFiltersRequest(), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(200); + }); + + test('catch error when finding rules throws error', async () => { + clients.rulesClient.find.mockImplementation(async () => { + throw new Error('Test error'); + }); + const response = await server.inject( + getRuleManagementFiltersRequest(), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: 'Test error', + status_code: 500, + }); + }); + }); + + describe('responses', () => { + test('1 rule installed, 1 custom rule and 3 tags', async () => { + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.rulesClient.aggregate.mockResolvedValue({ + alertExecutionStatus: {}, + ruleLastRunOutcome: {}, + ruleTags: ['a', 'b', 'c'], + }); + const request = getRuleManagementFiltersRequest(); + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + rules_summary: { + custom_count: 1, + prebuilt_installed_count: 1, + }, + aggregated_fields: { + tags: ['a', 'b', 'c'], + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/filters/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/filters/route.ts new file mode 100644 index 0000000000000..d7d974aa8d9ef --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/filters/route.ts @@ -0,0 +1,96 @@ +/* + * 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 { transformError } from '@kbn/securitysolution-es-utils'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; +import type { RulesClient } from '@kbn/alerting-plugin/server'; +import { RuleManagementFiltersResponse } from '../../../../../../../common/detection_engine/rule_management/api/rules/filters/response_schema'; +import { RULE_MANAGEMENT_FILTERS_URL } from '../../../../../../../common/detection_engine/rule_management/api/urls'; +import { buildSiemResponse } from '../../../../routes/utils'; +import type { SecuritySolutionPluginRouter } from '../../../../../../types'; +import { findRules } from '../../../logic/search/find_rules'; +import { readTags } from '../../tags/read_tags/read_tags'; + +interface RulesCount { + prebuilt: number; + custom: number; +} + +const DEFAULT_FIND_RULES_COUNT_PARAMS = { + perPage: 0, + page: 1, + sortField: undefined, + sortOrder: undefined, + fields: undefined, +}; + +async function fetchRulesCount(rulesClient: RulesClient): Promise { + const [prebuiltRules, customRules] = await Promise.all([ + findRules({ + ...DEFAULT_FIND_RULES_COUNT_PARAMS, + rulesClient, + filter: 'alert.attributes.params.immutable: true', + }), + findRules({ + ...DEFAULT_FIND_RULES_COUNT_PARAMS, + rulesClient, + filter: 'alert.attributes.params.immutable: false', + }), + ]); + + return { + prebuilt: prebuiltRules.total, + custom: customRules.total, + }; +} + +export const getRuleManagementFilters = (router: SecuritySolutionPluginRouter) => { + router.get( + { + path: RULE_MANAGEMENT_FILTERS_URL, + validate: false, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); + const ctx = await context.resolve(['alerting']); + const rulesClient = ctx.alerting.getRulesClient(); + + try { + const [{ prebuilt: prebuiltRulesCount, custom: customRulesCount }, tags] = + await Promise.all([fetchRulesCount(rulesClient), readTags({ rulesClient })]); + const responseBody: RuleManagementFiltersResponse = { + rules_summary: { + custom_count: customRulesCount, + prebuilt_installed_count: prebuiltRulesCount, + }, + aggregated_fields: { + tags, + }, + }; + const [validatedBody, validationError] = validate( + responseBody, + RuleManagementFiltersResponse + ); + + if (validationError != null) { + return siemResponse.error({ statusCode: 500, body: validationError }); + } else { + return response.ok({ body: validatedBody ?? {} }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/read_tags.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/read_tags.test.ts index ba9b00c01e3cf..1750cecdf1673 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/read_tags.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/read_tags.test.ts @@ -6,12 +6,7 @@ */ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; -import { - getRuleMock, - getFindResultWithMultiHits, -} from '../../../../routes/__mocks__/request_responses'; -import { getQueryRuleParams } from '../../../../rule_schema/mocks'; -import { readTags, convertTagsToSet, convertToTags, isTags } from './read_tags'; +import { readTags } from './read_tags'; describe('read_tags', () => { afterEach(() => { @@ -19,182 +14,16 @@ describe('read_tags', () => { }); describe('readTags', () => { - test('it should return the intersection of tags to where none are repeating', async () => { - const result1 = getRuleMock(getQueryRuleParams()); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 2', 'tag 3']; - - const result2 = getRuleMock(getQueryRuleParams()); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = ['tag 1', 'tag 2', 'tag 3', 'tag 4']; - - const rulesClient = rulesClientMock.create(); - rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - - const tags = await readTags({ rulesClient }); - expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); - }); - - test('it should return the intersection of tags to where some are repeating values', async () => { - const result1 = getRuleMock(getQueryRuleParams()); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; - - const result2 = getRuleMock(getQueryRuleParams()); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; - + test('it should return tags from the aggregation', async () => { const rulesClient = rulesClientMock.create(); - rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); + rulesClient.aggregate.mockResolvedValue({ + alertExecutionStatus: {}, + ruleLastRunOutcome: {}, + ruleTags: ['tag 1', 'tag 2', 'tag 3', 'tag 4'], + }); const tags = await readTags({ rulesClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); - - test('it should work with no tags defined between two results', async () => { - const result1 = getRuleMock(getQueryRuleParams()); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = []; - - const result2 = getRuleMock(getQueryRuleParams()); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = []; - - const rulesClient = rulesClientMock.create(); - rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - - const tags = await readTags({ rulesClient }); - expect(tags).toEqual([]); - }); - - test('it should work with a single tag which has repeating values in it', async () => { - const result1 = getRuleMock(getQueryRuleParams()); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 1', 'tag 1', 'tag 2']; - - const rulesClient = rulesClientMock.create(); - rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - - const tags = await readTags({ rulesClient }); - expect(tags).toEqual(['tag 1', 'tag 2']); - }); - - test('it should work with a single tag which has empty tags', async () => { - const result1 = getRuleMock(getQueryRuleParams()); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = []; - - const rulesClient = rulesClientMock.create(); - rulesClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - - const tags = await readTags({ rulesClient }); - expect(tags).toEqual([]); - }); - }); - - describe('convertTagsToSet', () => { - test('it should convert the intersection of two tag systems without duplicates', () => { - const result1 = getRuleMock(getQueryRuleParams()); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; - - const result2 = getRuleMock(getQueryRuleParams()); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; - - const findResult = getFindResultWithMultiHits({ data: [result1, result2] }); - const set = convertTagsToSet(findResult.data); - expect(Array.from(set)).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); - }); - - test('it should with with an empty array', () => { - const set = convertTagsToSet([]); - expect(Array.from(set)).toEqual([]); - }); - }); - - describe('convertToTags', () => { - test('it should convert the two tag systems together with duplicates', () => { - const result1 = getRuleMock(getQueryRuleParams()); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; - - const result2 = getRuleMock(getQueryRuleParams()); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; - - const findResult = getFindResultWithMultiHits({ data: [result1, result2] }); - const tags = convertToTags(findResult.data); - expect(tags).toEqual([ - 'tag 1', - 'tag 2', - 'tag 2', - 'tag 3', - 'tag 1', - 'tag 2', - 'tag 2', - 'tag 3', - 'tag 4', - ]); - }); - - test('it should filter out anything that is not a tag', () => { - const result1 = getRuleMock(getQueryRuleParams()); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; - - const result2 = getRuleMock(getQueryRuleParams()); - result2.id = '99979e67-19a7-455f-b452-8eded6135716'; - result2.params.ruleId = 'rule-2'; - // @ts-expect-error - delete result2.tags; - - const result3 = getRuleMock(getQueryRuleParams()); - result3.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result3.params.ruleId = 'rule-2'; - result3.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; - - const findResult = getFindResultWithMultiHits({ data: [result1, result2, result3] }); - const tags = convertToTags(findResult.data); - expect(tags).toEqual([ - 'tag 1', - 'tag 2', - 'tag 2', - 'tag 3', - 'tag 1', - 'tag 2', - 'tag 2', - 'tag 3', - 'tag 4', - ]); - }); - - test('it should with with an empty array', () => { - const tags = convertToTags([]); - expect(tags).toEqual([]); - }); - }); - - describe('isTags', () => { - test('it should return true if the object has a tags on it', () => { - expect(isTags({ tags: [] })).toBe(true); - }); - - test('it should return false if the object does not have a tags on it', () => { - expect(isTags({})).toBe(false); - }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/read_tags.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/read_tags.ts index 8819d95f366d8..1453bb4796246 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/read_tags.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/read_tags.ts @@ -5,65 +5,25 @@ * 2.0. */ -import { has } from 'lodash/fp'; import type { RulesClient } from '@kbn/alerting-plugin/server'; -import { findRules } from '../../../logic/search/find_rules'; +import { enrichFilterWithRuleTypeMapping } from '../../../logic/search/enrich_filter_with_rule_type_mappings'; -export interface TagType { - id: string; - tags: string[]; -} +// This is a contrived max limit on the number of tags. In fact it can exceed this number and will be truncated to the hardcoded number. +const EXPECTED_MAX_TAGS = 65536; -export const isTags = (obj: object): obj is TagType => { - return has('tags', obj); -}; - -export const convertToTags = (tagObjects: object[]): string[] => { - const tags = tagObjects.reduce((acc, tagObj) => { - if (isTags(tagObj)) { - return [...acc, ...tagObj.tags]; - } else { - return acc; - } - }, []); - return tags; -}; - -export const convertTagsToSet = (tagObjects: object[]): Set => { - return new Set(convertToTags(tagObjects)); -}; - -// Note: This is doing an in-memory aggregation of the tags by calling each of the alerting -// records in batches of this const setting and uses the fields to try to get the least -// amount of data per record back. If saved objects at some point supports aggregations -// then this should be replaced with a an aggregation call. -// Ref: https://www.elastic.co/guide/en/kibana/master/saved-objects-api.html export const readTags = async ({ rulesClient, }: { rulesClient: RulesClient; perPage?: number; }): Promise => { - // Get just one record so we can get the total count - const firstTags = await findRules({ - rulesClient, - fields: ['tags'], - perPage: 1, - page: 1, - sortField: 'createdAt', - sortOrder: 'desc', - filter: undefined, + const res = await rulesClient.aggregate({ + options: { + fields: ['tags'], + filter: enrichFilterWithRuleTypeMapping(undefined), + maxTags: EXPECTED_MAX_TAGS, + }, }); - // Get all the rules to aggregate over all the tags of the rules - const rules = await findRules({ - rulesClient, - fields: ['tags'], - perPage: firstTags.total, - sortField: 'createdAt', - sortOrder: 'desc', - page: 1, - filter: undefined, - }); - const tagSet = convertTagsToSet(rules.data); - return Array.from(tagSet); + + return res.ruleTags ?? []; }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 96e5c2ebfe0a8..f9c4359d9285f 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -27772,7 +27772,6 @@ "xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleSuccesDescription": "Installation effectuée des règles prépackagées à partir d'Elastic", "xpack.securitySolution.containers.detectionEngine.createPrePackagedTimelineSuccesDescription": "Installation effectuée des modèles de chronologies prépackagées à partir d'Elastic", "xpack.securitySolution.containers.detectionEngine.rulesAndTimelines": "Impossible de récupérer les règles et les chronologies", - "xpack.securitySolution.containers.detectionEngine.tagFetchFailDescription": "Impossible de récupérer les balises", "xpack.securitySolution.contextMenuItemByRouter.viewDetails": "Afficher les détails", "xpack.securitySolution.createPackagePolicy.stepConfigure.cloudDropdownOption": "Charges de travail cloud (serveurs Linux ou environnements Kubernetes)", "xpack.securitySolution.createPackagePolicy.stepConfigure.cloudEventFiltersAllEvents": "Tous les événements", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4280e72f3c6e0..2925afe9e3a1f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27745,7 +27745,6 @@ "xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleSuccesDescription": "Elastic から事前にパッケージ化されているルールをインストールしました", "xpack.securitySolution.containers.detectionEngine.createPrePackagedTimelineSuccesDescription": "Elasticから事前にパッケージ化されているタイムラインテンプレートをインストールしました", "xpack.securitySolution.containers.detectionEngine.rulesAndTimelines": "ルールとタイムラインを取得できませんでした", - "xpack.securitySolution.containers.detectionEngine.tagFetchFailDescription": "タグを取得できませんでした", "xpack.securitySolution.contextMenuItemByRouter.viewDetails": "詳細を表示", "xpack.securitySolution.createPackagePolicy.stepConfigure.cloudDropdownOption": "クラウドワークロード(LinuxサーバーまたはKubernetes環境)", "xpack.securitySolution.createPackagePolicy.stepConfigure.cloudEventFiltersAllEvents": "すべてのイベント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e7cf8542e470a..aac0c566a6732 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27778,7 +27778,6 @@ "xpack.securitySolution.containers.detectionEngine.createPrePackagedRuleSuccesDescription": "已安装 Elastic 的预打包规则", "xpack.securitySolution.containers.detectionEngine.createPrePackagedTimelineSuccesDescription": "安装 Elastic 预先打包的时间线模板", "xpack.securitySolution.containers.detectionEngine.rulesAndTimelines": "无法提取规则和时间线", - "xpack.securitySolution.containers.detectionEngine.tagFetchFailDescription": "无法提取标签", "xpack.securitySolution.contextMenuItemByRouter.viewDetails": "查看详情", "xpack.securitySolution.createPackagePolicy.stepConfigure.cloudDropdownOption": "云工作负载(Linux 服务器或 Kubernetes 环境)", "xpack.securitySolution.createPackagePolicy.stepConfigure.cloudEventFiltersAllEvents": "所有事件", diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts index ff24b25d89fa2..602bf75a7ef2b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts @@ -137,6 +137,35 @@ export default function createAggregateTests({ getService }: FtrProviderContext) }); }); + describe('tags limit', () => { + it('should be 50 be default', async () => { + const numOfAlerts = 3; + const numOfTagsPerAlert = 30; + + await Promise.all( + [...Array(numOfAlerts)].map(async (_, alertIndex) => { + const okAlertId = await createTestAlert( + { + rule_type_id: 'test.noop', + schedule: { interval: '1s' }, + tags: [...Array(numOfTagsPerAlert)].map( + (__, i) => `tag-${i + numOfTagsPerAlert * alertIndex}` + ), + }, + 'ok' + ); + objectRemover.add(Spaces.space1.id, okAlertId, 'rule', 'alerting'); + }) + ); + + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_aggregate` + ); + + expect(response.body.rule_tags.length).to.eql(50); + }); + }); + describe('legacy', () => { it('should aggregate alert status totals', async () => { const NumOkAlerts = 4; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_rule_management_filters.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_rule_management_filters.ts new file mode 100644 index 0000000000000..9d95505e3db65 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_rule_management_filters.ts @@ -0,0 +1,113 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { RULE_MANAGEMENT_FILTERS_URL } from '@kbn/security-solution-plugin/common/detection_engine/rule_management/api/urls'; +import { PREBUILT_RULES_URL } from '@kbn/security-solution-plugin/common/detection_engine/prebuilt_rules'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { deleteAllAlerts, getSimpleRule } from '../../utils'; +import { createPrebuiltRuleAssetSavedObjects } from '../../utils/create_prebuilt_rule_saved_objects'; +import { deleteAllPrebuiltRules } from '../../utils/delete_all_prebuilt_rules'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const log = getService('log'); + + describe('get_rule_management_filters', () => { + beforeEach(async () => { + await deleteAllAlerts(supertest, log); + }); + + it('should return the correct result when there are no rules', async () => { + const { body } = await supertest + .get(RULE_MANAGEMENT_FILTERS_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({ + rules_summary: { + custom_count: 0, + prebuilt_installed_count: 0, + }, + aggregated_fields: { + tags: [], + }, + }); + }); + + describe('when there is a custom rule', () => { + beforeEach(async () => { + const rule = getSimpleRule(); + rule.tags = ['tag-a']; + + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + }); + + it('should return the correct number of custom rules', async () => { + const { body } = await supertest + .get(RULE_MANAGEMENT_FILTERS_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.rules_summary.custom_count).to.eql(1); + expect(body.rules_summary.prebuilt_installed_count).to.eql(0); + }); + + it('should return correct tags', async () => { + const { body } = await supertest + .get(RULE_MANAGEMENT_FILTERS_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.aggregated_fields.tags).to.eql(['tag-a']); + }); + }); + + describe('when there are installed prebuilt rules', () => { + beforeEach(async () => { + await createPrebuiltRuleAssetSavedObjects(es); + await supertest.put(PREBUILT_RULES_URL).set('kbn-xsrf', 'true').send().expect(200); + }); + + afterEach(async () => { + await deleteAllPrebuiltRules(es); + }); + + it('should return the correct number of installed prepacked rules after pre-packaged rules have been installed', async () => { + const { body } = await supertest + .get(RULE_MANAGEMENT_FILTERS_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.rules_summary.prebuilt_installed_count).to.eql(3); + expect(body.rules_summary.custom_count).to.eql(0); + }); + + it('should return installed prebuilt rules tags', async () => { + const { body } = await supertest + .get(RULE_MANAGEMENT_FILTERS_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.aggregated_fields.tags).to.eql(['test-tag-1', 'test-tag-2', 'test-tag-3']); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts index 690498a287530..24e0d5389a018 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts @@ -31,5 +31,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./find_rules')); loadTestFile(require.resolve('./find_rule_exception_references')); loadTestFile(require.resolve('./get_prepackaged_rules_status')); + loadTestFile(require.resolve('./get_rule_management_filters')); }); }; diff --git a/x-pack/test/detection_engine_api_integration/utils/create_prebuilt_rule_saved_objects.ts b/x-pack/test/detection_engine_api_integration/utils/create_prebuilt_rule_saved_objects.ts index 15778e41a3be6..c97f97d3a62a3 100644 --- a/x-pack/test/detection_engine_api_integration/utils/create_prebuilt_rule_saved_objects.ts +++ b/x-pack/test/detection_engine_api_integration/utils/create_prebuilt_rule_saved_objects.ts @@ -21,6 +21,7 @@ export const SAMPLE_PREBUILT_RULES = [ 'security-rule': { ...getPrebuiltRuleWithExceptionsMock(), rule_id: ELASTIC_SECURITY_RULE_ID, + tags: ['test-tag-1'], enabled: true, }, type: 'security-rule', @@ -33,6 +34,7 @@ export const SAMPLE_PREBUILT_RULES = [ 'security-rule': { ...getPrebuiltRuleMock(), rule_id: '000047bb-b27a-47ec-8b62-ef1a5d2c9e19', + tags: ['test-tag-2'], }, type: 'security-rule', references: [], @@ -44,6 +46,7 @@ export const SAMPLE_PREBUILT_RULES = [ 'security-rule': { ...getPrebuiltRuleMock(), rule_id: '00140285-b827-4aee-aa09-8113f58a08f3', + tags: ['test-tag-3'], }, type: 'security-rule', references: [], From 92ffe2764f01a67658db854506bbd5003f0adcb8 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 20 Dec 2022 09:19:11 +0100 Subject: [PATCH 37/55] [ML] Explain Log Rate Spikes: Fix client side code to transform groups into table rows. (#147592) Fixes client side code to transform groups into table rows. Because the transformation used a dictionary like structure with field names as keys, we missed if there were multiple values for a field. This changes the structure to an array of field/value pairs so we can support multiple values per field. --- .../application/utils/query_utils.test.ts | 20 ++-- .../public/application/utils/query_utils.ts | 11 +-- .../explain_log_rate_spikes_analysis.tsx | 11 ++- .../spike_analysis_table_groups.tsx | 92 +++++++++---------- .../test/functional/apps/aiops/test_data.ts | 2 +- 5 files changed, 67 insertions(+), 69 deletions(-) diff --git a/x-pack/plugins/aiops/public/application/utils/query_utils.test.ts b/x-pack/plugins/aiops/public/application/utils/query_utils.test.ts index 59d680d236b15..c886b16fa0ec2 100644 --- a/x-pack/plugins/aiops/public/application/utils/query_utils.test.ts +++ b/x-pack/plugins/aiops/public/application/utils/query_utils.test.ts @@ -27,16 +27,16 @@ const selectedGroupMock: GroupTableItem = { id: '21289599', docCount: 20468, pValue: 2.2250738585072626e-308, - group: { - 'error.message': 'rate limit exceeded', - message: 'too many requests', - 'user_agent.original.keyword': 'Mozilla/5.0', - }, - repeatedValues: { - 'beat.hostname.keyword': 'ip-192-168-1-1', - 'beat.name.keyword': 'i-1234', - 'docker.container.id.keyword': 'asdf', - }, + group: [ + { fieldName: 'error.message', fieldValue: 'rate limit exceeded' }, + { fieldName: 'message', fieldValue: 'too many requests' }, + { fieldName: 'user_agent.original.keyword', fieldValue: 'Mozilla/5.0' }, + ], + repeatedValues: [ + { fieldName: 'beat.hostname.keyword', fieldValue: 'ip-192-168-1-1' }, + { fieldName: 'beat.name.keyword', fieldValue: 'i-1234' }, + { fieldName: 'docker.container.id.keyword', fieldValue: 'asdf' }, + ], histogram: [], }; diff --git a/x-pack/plugins/aiops/public/application/utils/query_utils.ts b/x-pack/plugins/aiops/public/application/utils/query_utils.ts index 1779b434df508..0c0363d852bc9 100644 --- a/x-pack/plugins/aiops/public/application/utils/query_utils.ts +++ b/x-pack/plugins/aiops/public/application/utils/query_utils.ts @@ -15,7 +15,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { Query } from '@kbn/es-query'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; -import type { ChangePoint } from '@kbn/ml-agg-utils'; +import type { ChangePoint, FieldValuePair } from '@kbn/ml-agg-utils'; import type { GroupTableItem } from '../../components/spike_analysis_table/spike_analysis_table_groups'; /* @@ -52,11 +52,10 @@ export function buildBaseFilterCriteria( const groupFilter = []; if (selectedGroup) { - const allItems = { ...selectedGroup.group, ...selectedGroup.repeatedValues }; - for (const fieldName in allItems) { - if (allItems.hasOwnProperty(fieldName)) { - groupFilter.push({ term: { [fieldName]: allItems[fieldName] } }); - } + const allItems: FieldValuePair[] = [...selectedGroup.group, ...selectedGroup.repeatedValues]; + for (const item of allItems) { + const { fieldName, fieldValue } = item; + groupFilter.push({ term: { [fieldName]: fieldValue } }); } } diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx index 4b8ad8a891961..e84f50b02711c 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx @@ -25,6 +25,7 @@ import type { WindowParameters } from '@kbn/aiops-utils'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { Query } from '@kbn/es-query'; +import type { FieldValuePair } from '@kbn/ml-agg-utils'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { initialState, streamReducer } from '../../../common/api/stream_reducer'; @@ -163,15 +164,15 @@ export const ExplainLogRateSpikesAnalysis: FC const sortedGroup = group.sort((a, b) => a.fieldName > b.fieldName ? 1 : b.fieldName > a.fieldName ? -1 : 0 ); - const dedupedGroup: Record = {}; - const repeatedValues: Record = {}; + const dedupedGroup: FieldValuePair[] = []; + const repeatedValues: FieldValuePair[] = []; sortedGroup.forEach((pair) => { const { fieldName, fieldValue } = pair; if (pair.duplicate === false) { - dedupedGroup[fieldName] = fieldValue; + dedupedGroup.push({ fieldName, fieldValue }); } else { - repeatedValues[fieldName] = fieldValue; + repeatedValues.push({ fieldName, fieldValue }); } }); @@ -197,7 +198,7 @@ export const ExplainLogRateSpikesAnalysis: FC const showSpikeAnalysisTable = data?.changePoints.length > 0; const groupItemCount = groupTableItems.reduce((p, c) => { - return p + Object.keys(c.group).length; + return p + c.group.length; }, 0); const foundGroups = groupTableItems.length > 0 && groupItemCount > 0; diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx index 7f628a7dfaa66..2551859853771 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx @@ -27,7 +27,7 @@ import { import { i18n } from '@kbn/i18n'; import { escapeKuery } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { ChangePoint } from '@kbn/ml-agg-utils'; +import type { ChangePoint, FieldValuePair } from '@kbn/ml-agg-utils'; import { SEARCH_QUERY_LANGUAGE } from '../../application/utils/search_utils'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; @@ -58,8 +58,8 @@ export interface GroupTableItem { id: string; docCount: number; pValue: number | null; - group: Record; - repeatedValues: Record; + group: FieldValuePair[]; + repeatedValues: FieldValuePair[]; histogram: ChangePoint['histogram']; } @@ -99,23 +99,22 @@ export const SpikeAnalysisGroupsTable: FC = ({ const { group, repeatedValues } = item; const expandedTableItems = []; - const fullGroup = { ...group, ...repeatedValues }; - - for (const fieldName in fullGroup) { - if (fullGroup.hasOwnProperty(fieldName)) { - const fieldValue = fullGroup[fieldName]; - expandedTableItems.push({ - fieldName: `${fieldName}`, - fieldValue: `${fullGroup[fieldName]}`, - ...(changePoints.find( - (changePoint) => - (changePoint.fieldName === fieldName || - changePoint.fieldName === `${fieldName}.keyword`) && - (changePoint.fieldValue === fieldValue || - changePoint.fieldValue === `${fieldValue}.keyword`) - ) ?? {}), - }); - } + const fullGroup: FieldValuePair[] = [...group, ...repeatedValues]; + + for (const fullGroupItem of fullGroup) { + const { fieldName, fieldValue } = fullGroupItem; + + expandedTableItems.push({ + ...(changePoints.find( + (changePoint) => + (changePoint.fieldName === fieldName || + changePoint.fieldName === `${fieldName}.keyword`) && + (changePoint.fieldValue === fieldValue || + changePoint.fieldValue === `${fieldValue}.keyword`) + ) ?? {}), + fieldName: `${fieldName}`, + fieldValue: `${fieldValue}`, + }); } itemIdToExpandedRowMapValues[item.id] = ( @@ -178,12 +177,12 @@ export const SpikeAnalysisGroupsTable: FC = ({ query: { language: SEARCH_QUERY_LANGUAGE.KUERY, query: [ - ...Object.entries(groupTableItem.group).map( - ([fieldName, fieldValue]) => + ...groupTableItem.group.map( + ({ fieldName, fieldValue }) => `${escapeKuery(fieldName)}:${escapeKuery(String(fieldValue))}` ), - ...Object.entries(groupTableItem.repeatedValues).map( - ([fieldName, fieldValue]) => + ...groupTableItem.repeatedValues.map( + ({ fieldName, fieldValue }) => `${escapeKuery(fieldName)}:${escapeKuery(String(fieldValue))}` ), ].join(' AND '), @@ -253,27 +252,26 @@ export const SpikeAnalysisGroupsTable: FC = ({ ), render: (_, { group, repeatedValues }) => { const valuesBadges = []; - const hasExtraBadges = Object.keys(group).length > MAX_GROUP_BADGES; - - for (const fieldName in group) { - if (group.hasOwnProperty(fieldName)) { - if (valuesBadges.length === MAX_GROUP_BADGES) break; - valuesBadges.push( - <> - - {`${fieldName}: `} - {`${group[fieldName]}`} - - - - ); - } + const hasExtraBadges = group.length > MAX_GROUP_BADGES; + + for (const groupItem of group) { + const { fieldName, fieldValue } = groupItem; + if (valuesBadges.length === MAX_GROUP_BADGES) break; + valuesBadges.push( + <> + + {`${fieldName}: `} + {`${fieldValue}`} + + + + ); } - if (Object.keys(repeatedValues).length > 0 || hasExtraBadges) { + if (repeatedValues.length > 0 || hasExtraBadges) { valuesBadges.push( <> = ({
    ) : null} - {Object.keys(repeatedValues).length > 0 ? ( + {repeatedValues.length > 0 ? ( ) : null}
    diff --git a/x-pack/test/functional/apps/aiops/test_data.ts b/x-pack/test/functional/apps/aiops/test_data.ts index 4a07dcc8a7433..f544225f14c97 100644 --- a/x-pack/test/functional/apps/aiops/test_data.ts +++ b/x-pack/test/functional/apps/aiops/test_data.ts @@ -55,7 +55,7 @@ export const artificialLogDataViewTestData: TestData = { totalDocCountFormatted: '8,400', analysisGroupsTable: [ { group: 'user: Peter', docCount: '1981' }, - { group: 'response_code: 500url: login.php', docCount: '792' }, + { group: 'response_code: 500url: home.phpurl: login.php', docCount: '792' }, ], analysisTable: [ { From 792fe5eda86c19e8d8e4dffabc3f9f2575993833 Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Tue, 20 Dec 2022 08:44:04 +0000 Subject: [PATCH 38/55] [Fleet] Add active filter count to agent status filter (#147821) ## Summary The agent status filter previously did not have the badge with the number of available or active filter on it. Here it is now with the badge: Screenshot 2022-12-19 at 22 51 52 Screenshot 2022-12-19 at 22 53 33 --- .../agents/agent_list_page/components/search_and_filter_bar.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx index 9b16136df2d96..b7479738e9029 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx @@ -225,6 +225,8 @@ export const SearchAndFilterBar: React.FunctionComponent<{ onClick={() => setIsStatusFilterOpen(!isStatusFilterOpen)} isSelected={isStatusFilterOpen} hasActiveFilters={selectedStatus.length > 0} + numActiveFilters={selectedStatus.length} + numFilters={statusFilters.length} disabled={agentPolicies.length === 0} data-test-subj="agentList.statusFilter" > From c673938fbf901ae6f68e044dc46ea4e6b7c2aac8 Mon Sep 17 00:00:00 2001 From: Muhammad Ibragimov <53621505+mibragimov@users.noreply.github.com> Date: Tue, 20 Dec 2022 13:50:38 +0500 Subject: [PATCH 39/55] [Console] Fix parsing of uuids in variables (#147601) Fixes https://github.com/elastic/kibana/issues/145764 ### Summary This PR fixes the parsing of uuids in variables. Previously, uuids were parsed as numbers, which caused the request to fail. This PR fixes that by checking if the value is a uuid before parsing it as a number. Co-authored-by: Muhammad Ibragimov Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/console/public/lib/utils/index.ts | 10 +++++++++- src/plugins/console/public/lib/utils/utils.test.js | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/plugins/console/public/lib/utils/index.ts b/src/plugins/console/public/lib/utils/index.ts index dfa513085019d..340ccc9eab6e8 100644 --- a/src/plugins/console/public/lib/utils/index.ts +++ b/src/plugins/console/public/lib/utils/index.ts @@ -147,7 +147,8 @@ export const replaceVariables = ( } const isStringifiedNumber = !isNaN(parseFloat(value)); - if (isStringifiedNumber) { + // We need to check uuids as well, since they are also numbers. + if (isStringifiedNumber && !isUUID(value)) { return value; } @@ -175,3 +176,10 @@ export const replaceVariables = ( return req; }); }; + +const isUUID = (val: string) => { + return ( + typeof val === 'string' && + val.match(/[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}/) + ); +}; diff --git a/src/plugins/console/public/lib/utils/utils.test.js b/src/plugins/console/public/lib/utils/utils.test.js index 406411b7a32bf..39632f33bdb87 100644 --- a/src/plugins/console/public/lib/utils/utils.test.js +++ b/src/plugins/console/public/lib/utils/utils.test.js @@ -255,5 +255,13 @@ describe('Utils class', () => { { url: 'test', data: ['{\n "f": {"v1": "${v1}", "v6": "${v6}"}\n}'] } ); }); + + describe('with uuids as field values', () => { + testVariables( + { url: 'test', data: ['{\n "f": "${v7}"\n}'] }, + { name: 'v7', value: '9893617a-a08f-4e5c-bc41-95610dc2ded8' }, + { url: 'test', data: ['{\n "f": "9893617a-a08f-4e5c-bc41-95610dc2ded8"\n}'] } + ); + }); }); }); From 3cfada5a0322cfc319ae2cde2e34b40f94d68297 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Tue, 20 Dec 2022 09:53:04 +0100 Subject: [PATCH 40/55] [SLO] SLO List (#147447) Closes https://github.com/elastic/kibana/issues/146892 --- .../utils/formatters/formatters.test.ts | 35 +- .../common/utils/formatters/formatters.ts | 21 + .../slo/slo_selector/slo_selector.test.tsx | 4 +- .../shared/slo/slo_selector/slo_selector.tsx | 2 +- .../observability/public/config/paths.ts | 1 + .../__storybook_mocks__/use_fetch_slo_list.ts | 1 + .../public/hooks/slo/use_delete_slo.ts | 46 + .../public/hooks/slo/use_fetch_slo_list.ts | 47 +- .../public/hooks/use_data_fetcher.ts | 16 +- .../slos/components/assets/illustration.svg | 920 ++++++++++++++++++ .../slos/components/slo_badges.stories.tsx | 30 + .../pages/slos/components/slo_badges.tsx | 37 + .../slo_delete_confirmation_modal.stories.tsx | 33 + .../slo_delete_confirmation_modal.tsx | 104 ++ .../slos/components/slo_list.stories.tsx | 2 + .../public/pages/slos/components/slo_list.tsx | 95 +- .../components/slo_list_empty.stories.tsx | 25 + .../pages/slos/components/slo_list_empty.tsx | 26 + .../components/slo_list_error.stories.tsx | 25 + .../pages/slos/components/slo_list_error.tsx | 34 + .../slos/components/slo_list_item.stories.tsx | 30 + .../pages/slos/components/slo_list_item.tsx | 141 +++ .../components/slo_list_items.stories.tsx | 36 + .../pages/slos/components/slo_list_items.tsx | 52 + ...lo_list_search_filter_sort_bar.stories.tsx | 35 + .../slo_list_search_filter_sort_bar.tsx | 192 ++++ .../components/slo_list_welcome_prompt.tsx | 80 ++ .../components/slo_summary_stats.stories.tsx | 29 + .../slos/components/slo_summary_stats.tsx | 78 ++ .../public/pages/slos/helpers/filter_slos.ts | 25 + .../pages/slos/helpers/get_slo_difference.ts | 17 + .../pages/slos/helpers/is_slo_healthy.ts | 11 + .../public/pages/slos/helpers/sort_slos.ts | 23 + .../public/pages/slos/index.test.tsx | 33 +- .../observability/public/pages/slos/index.tsx | 21 +- x-pack/plugins/observability/public/plugin.ts | 12 +- .../public/update_global_navigation.test.tsx | 86 +- .../public/update_global_navigation.tsx | 5 + .../kibana_react.storybook_decorator.tsx | 31 + .../apps/apm/feature_controls/apm_security.ts | 2 + .../infrastructure_security.ts | 16 +- .../infra/feature_controls/logs_security.ts | 4 +- .../feature_controls/uptime_security.ts | 10 +- 43 files changed, 2403 insertions(+), 70 deletions(-) create mode 100644 x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts create mode 100644 x-pack/plugins/observability/public/pages/slos/components/assets/illustration.svg create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_badges.stories.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_badges.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_delete_confirmation_modal.stories.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_delete_confirmation_modal.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_list_empty.stories.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_list_empty.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_list_error.stories.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_list_error.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_list_item.stories.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_list_items.stories.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.stories.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_list_welcome_prompt.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_summary_stats.stories.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/components/slo_summary_stats.tsx create mode 100644 x-pack/plugins/observability/public/pages/slos/helpers/filter_slos.ts create mode 100644 x-pack/plugins/observability/public/pages/slos/helpers/get_slo_difference.ts create mode 100644 x-pack/plugins/observability/public/pages/slos/helpers/is_slo_healthy.ts create mode 100644 x-pack/plugins/observability/public/pages/slos/helpers/sort_slos.ts create mode 100644 x-pack/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx diff --git a/x-pack/plugins/observability/common/utils/formatters/formatters.test.ts b/x-pack/plugins/observability/common/utils/formatters/formatters.test.ts index 397eaae04b51a..87518ead5925f 100644 --- a/x-pack/plugins/observability/common/utils/formatters/formatters.test.ts +++ b/x-pack/plugins/observability/common/utils/formatters/formatters.test.ts @@ -5,7 +5,13 @@ * 2.0. */ -import { asDecimal, asInteger, asPercent, asDecimalOrInteger } from './formatters'; +import { + asDecimal, + asInteger, + asPercent, + asPercentWithTwoDecimals, + asDecimalOrInteger, +} from './formatters'; describe('formatters', () => { describe('asDecimal', () => { @@ -89,6 +95,33 @@ describe('formatters', () => { }); }); + describe('asPercentWithTwoDecimals', () => { + it('formats as integer when number is above 10', () => { + expect(asPercentWithTwoDecimals(3725, 10000, 'n/a')).toEqual('37.25%'); + }); + + it('adds a decimal when value is below 10', () => { + expect(asPercentWithTwoDecimals(0.092, 1)).toEqual('9.20%'); + }); + + it('formats when numerator is 0', () => { + expect(asPercentWithTwoDecimals(0, 1, 'n/a')).toEqual('0%'); + }); + + it('returns fallback when denominator is undefined', () => { + expect(asPercentWithTwoDecimals(3725, undefined, 'n/a')).toEqual('n/a'); + }); + + it('returns fallback when denominator is 0 ', () => { + expect(asPercentWithTwoDecimals(3725, 0, 'n/a')).toEqual('n/a'); + }); + + it('returns fallback when numerator or denominator is NaN', () => { + expect(asPercentWithTwoDecimals(3725, NaN, 'n/a')).toEqual('n/a'); + expect(asPercentWithTwoDecimals(NaN, 10000, 'n/a')).toEqual('n/a'); + }); + }); + describe('asDecimalOrInteger', () => { it('formats as integer when number equals to 0 ', () => { expect(asDecimalOrInteger(0)).toEqual('0'); diff --git a/x-pack/plugins/observability/common/utils/formatters/formatters.ts b/x-pack/plugins/observability/common/utils/formatters/formatters.ts index 05d8d2638ba7b..2b435458d2494 100644 --- a/x-pack/plugins/observability/common/utils/formatters/formatters.ts +++ b/x-pack/plugins/observability/common/utils/formatters/formatters.ts @@ -47,6 +47,27 @@ export function asPercent( return numeral(decimal).format('0.0%'); } +export function asPercentWithTwoDecimals( + numerator: Maybe, + denominator: number | undefined, + fallbackResult = NOT_AVAILABLE_LABEL +) { + if (!denominator || !isFiniteNumber(numerator)) { + return fallbackResult; + } + + const decimal = numerator / denominator; + + // 33.2 => 33.20% + // 3.32 => 3.32% + // 0 => 0% + if (String(Math.abs(decimal)).split('.').at(1)?.length === 2 || decimal === 0 || decimal === 1) { + return numeral(decimal).format('0%'); + } + + return numeral(decimal).format('0.00%'); +} + export type AsPercent = typeof asPercent; export function asDecimalOrInteger(value: number) { diff --git a/x-pack/plugins/observability/public/components/shared/slo/slo_selector/slo_selector.test.tsx b/x-pack/plugins/observability/public/components/shared/slo/slo_selector/slo_selector.test.tsx index 5ac7d6857f19d..94028c11b1bf4 100644 --- a/x-pack/plugins/observability/public/components/shared/slo/slo_selector/slo_selector.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/slo/slo_selector/slo_selector.test.tsx @@ -29,7 +29,7 @@ describe('SLO Selector', () => { render(); expect(screen.getByTestId('sloSelector')).toBeTruthy(); - expect(useFetchSloListMock).toHaveBeenCalledWith(''); + expect(useFetchSloListMock).toHaveBeenCalledWith({ name: '', refetch: false }); }); it('searches SLOs when typing', async () => { @@ -41,6 +41,6 @@ describe('SLO Selector', () => { await wait(310); // debounce delay }); - expect(useFetchSloListMock).toHaveBeenCalledWith('latency'); + expect(useFetchSloListMock).toHaveBeenCalledWith({ name: 'latency', refetch: false }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/slo/slo_selector/slo_selector.tsx b/x-pack/plugins/observability/public/components/shared/slo/slo_selector/slo_selector.tsx index 52db0b45766c5..61fc6eb20bc38 100644 --- a/x-pack/plugins/observability/public/components/shared/slo/slo_selector/slo_selector.tsx +++ b/x-pack/plugins/observability/public/components/shared/slo/slo_selector/slo_selector.tsx @@ -21,7 +21,7 @@ function SloSelector({ onSelected }: Props) { const [options, setOptions] = useState>>([]); const [selectedOptions, setSelectedOptions] = useState>>(); const [searchValue, setSearchValue] = useState(''); - const { loading, sloList } = useFetchSloList(searchValue); + const { loading, sloList } = useFetchSloList({ name: searchValue, refetch: false }); useEffect(() => { const isLoadedWithData = !loading && sloList !== undefined; diff --git a/x-pack/plugins/observability/public/config/paths.ts b/x-pack/plugins/observability/public/config/paths.ts index 7108887ca12d7..76f621082bf32 100644 --- a/x-pack/plugins/observability/public/config/paths.ts +++ b/x-pack/plugins/observability/public/config/paths.ts @@ -18,6 +18,7 @@ export const paths = { ruleDetails: (ruleId?: string | null) => ruleId ? `${RULES_PAGE_LINK}/${encodeURI(ruleId)}` : RULES_PAGE_LINK, slos: SLOS_PAGE_LINK, + sloDetails: (sloId: string) => `${SLOS_PAGE_LINK}/${encodeURI(sloId)}`, }, management: { rules: '/app/management/insightsAndAlerting/triggersActions/rules', diff --git a/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_slo_list.ts b/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_slo_list.ts index 851a4a36788b5..4a2baf3ceedba 100644 --- a/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_slo_list.ts +++ b/x-pack/plugins/observability/public/hooks/slo/__storybook_mocks__/use_fetch_slo_list.ts @@ -11,6 +11,7 @@ import { UseFetchSloListResponse } from '../use_fetch_slo_list'; export const useFetchSloList = (name?: string): UseFetchSloListResponse => { return { loading: false, + error: false, sloList, }; }; diff --git a/x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts b/x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts new file mode 100644 index 0000000000000..50186923307c6 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/slo/use_delete_slo.ts @@ -0,0 +1,46 @@ +/* + * 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 { useCallback, useState } from 'react'; +import { useKibana } from '../../utils/kibana_react'; + +interface UseDeleteSlo { + loading: boolean; + success: boolean; + error: string | undefined; + deleteSlo: (id: string) => void; +} + +export function useDeleteSlo(): UseDeleteSlo { + const { http } = useKibana().services; + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + const [error, setError] = useState(undefined); + + const deleteSlo = useCallback( + async (id: string) => { + setLoading(true); + setError(''); + setSuccess(false); + + try { + await http.delete(`/api/observability/slos/${id}`); + setSuccess(true); + } catch (e) { + setError(e); + } + }, + [http] + ); + + return { + loading, + error, + success, + deleteSlo, + }; +} diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts index e6900ea5181d6..d9be2bed01535 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_list.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { HttpSetup } from '@kbn/core/public'; import type { SLOList } from '../../typings/slo'; import { useDataFetcher } from '../use_data_fetcher'; import { toSLO } from '../../utils/slo/slo'; -const EMPTY_LIST = { +const EMPTY_LIST: SLOList = { results: [], total: 0, page: 0, @@ -21,29 +21,46 @@ const EMPTY_LIST = { interface SLOListParams { name?: string; + page?: number; } -interface UseFetchSloListResponse { - loading: boolean; +export interface UseFetchSloListResponse { sloList: SLOList; + loading: boolean; + error: boolean; } -const useFetchSloList = (name?: string): UseFetchSloListResponse => { - const params: SLOListParams = useMemo(() => ({ name }), [name]); +export function useFetchSloList({ + name, + refetch, + page, +}: { + refetch: boolean; + name?: string; + page?: number; +}): UseFetchSloListResponse { + const [sloList, setSloList] = useState(EMPTY_LIST); + + const params: SLOListParams = useMemo(() => ({ name, page }), [name, page]); const shouldExecuteApiCall = useCallback( - (apiCallParams: SLOListParams) => apiCallParams.name === params.name, - [params] + (apiCallParams: SLOListParams) => + apiCallParams.name === params.name || apiCallParams.page === params.page || refetch, + [params, refetch] ); - const { loading, data: sloList } = useDataFetcher({ + const { data, loading, error } = useDataFetcher({ paramsForApiCall: params, - initialDataState: EMPTY_LIST, + initialDataState: sloList, executeApiCall: fetchSloList, shouldExecuteApiCall, }); - return { loading, sloList }; -}; + useEffect(() => { + setSloList(data); + }, [data]); + + return { sloList, loading, error }; +} const fetchSloList = async ( params: SLOListParams, @@ -53,6 +70,7 @@ const fetchSloList = async ( try { const response = await http.get>(`/api/observability/slos`, { query: { + ...(params.page && { page: params.page }), ...(params.name && { name: params.name }), }, signal: abortController.signal, @@ -73,12 +91,9 @@ function toSLOList(response: Record): SLOList { } return { - results: response.results.map((result) => toSLO(result)), + results: response.results.map(toSLO), page: Number(response.page), perPage: Number(response.per_page), total: Number(response.total), }; } - -export { useFetchSloList }; -export type { UseFetchSloListResponse }; diff --git a/x-pack/plugins/observability/public/hooks/use_data_fetcher.ts b/x-pack/plugins/observability/public/hooks/use_data_fetcher.ts index cb71da0919d70..8e2303ac04e87 100644 --- a/x-pack/plugins/observability/public/hooks/use_data_fetcher.ts +++ b/x-pack/plugins/observability/public/hooks/use_data_fetcher.ts @@ -25,6 +25,7 @@ export const useDataFetcher = ({ }) => { const { http } = useKibana().services; const [loading, setLoading] = useState(false); + const [error, setError] = useState(false); const [data, setData] = useState(initialDataState); const { fetch, cancel } = useMemo(() => { @@ -34,12 +35,18 @@ export const useDataFetcher = ({ return { fetch: async () => { if (shouldExecuteApiCall(paramsForApiCall)) { + setError(false); setLoading(true); - const results = await executeApiCall(paramsForApiCall, abortController, http); - if (!isCanceled) { + try { + const results = await executeApiCall(paramsForApiCall, abortController, http); + if (!isCanceled) { + setLoading(false); + setData(results); + } + } catch (e) { + setError(true); setLoading(false); - setData(results); } } }, @@ -59,7 +66,8 @@ export const useDataFetcher = ({ }, [fetch, cancel]); return { - loading, data, + loading, + error, }; }; diff --git a/x-pack/plugins/observability/public/pages/slos/components/assets/illustration.svg b/x-pack/plugins/observability/public/pages/slos/components/assets/illustration.svg new file mode 100644 index 0000000000000..363c492fec84b --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/assets/illustration.svg @@ -0,0 +1,920 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_badges.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_badges.stories.tsx new file mode 100644 index 0000000000000..78af306d5c165 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_badges.stories.tsx @@ -0,0 +1,30 @@ +/* + * 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 from 'react'; +import { ComponentStory } from '@storybook/react'; + +import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator'; +import { SloBadges as Component, SloBadgesProps } from './slo_badges'; +import { anSLO } from '../../../../common/data/slo'; + +export default { + component: Component, + title: 'app/SLO/ListPage/SloBadges', + decorators: [KibanaReactStorybookDecorator], +}; + +const Template: ComponentStory = (props: SloBadgesProps) => ( + +); + +const defaultProps = { + slo: anSLO, +}; + +export const SloBadges = Template.bind({}); +SloBadges.args = defaultProps; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_badges.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_badges.tsx new file mode 100644 index 0000000000000..7af1724c49dda --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_badges.tsx @@ -0,0 +1,37 @@ +/* + * 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 from 'react'; +import { EuiBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { euiLightVars } from '@kbn/ui-theme'; +import { isSloHealthy } from '../helpers/is_slo_healthy'; +import { SLO } from '../../../typings'; + +export interface SloBadgesProps { + slo: SLO; +} + +export function SloBadges({ slo }: SloBadgesProps) { + return ( + <> + {isSloHealthy(slo) ? ( + + {i18n.translate('xpack.observability.slos.slo.state.healthy', { + defaultMessage: 'Healthy', + })} + + ) : ( + + {i18n.translate('xpack.observability.slos.slo.state.violated', { + defaultMessage: 'Violated', + })} + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_delete_confirmation_modal.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_delete_confirmation_modal.stories.tsx new file mode 100644 index 0000000000000..27ad207ccb9f9 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_delete_confirmation_modal.stories.tsx @@ -0,0 +1,33 @@ +/* + * 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 from 'react'; +import { ComponentStory } from '@storybook/react'; + +import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator'; +import { + SloDeleteConfirmationModal as Component, + SloDeleteConfirmationModalProps, +} from './slo_delete_confirmation_modal'; +import { anSLO } from '../../../../common/data/slo'; + +export default { + component: Component, + title: 'app/SLO/ListPage/SloDeleteConfirmationModal', + decorators: [KibanaReactStorybookDecorator], +}; + +const Template: ComponentStory = (props: SloDeleteConfirmationModalProps) => ( + +); + +const defaultProps = { + slo: anSLO, +}; + +export const SloDeleteConfirmationModal = Template.bind({}); +SloDeleteConfirmationModal.args = defaultProps; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_delete_confirmation_modal.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_delete_confirmation_modal.tsx new file mode 100644 index 0000000000000..e69a971232f26 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_delete_confirmation_modal.tsx @@ -0,0 +1,104 @@ +/* + * 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 { EuiConfirmModal } from '@elastic/eui'; +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../utils/kibana_react'; +import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo'; +import { SLO } from '../../../typings'; + +export interface SloDeleteConfirmationModalProps { + slo: SLO; + onCancel: () => void; + onDeleting: () => void; + onDeleted: () => void; +} + +export function SloDeleteConfirmationModal({ + slo: { id, name }, + onCancel, + onDeleting, + onDeleted, +}: SloDeleteConfirmationModalProps) { + const { + notifications: { toasts }, + } = useKibana().services; + + const [isVisible, setIsVisible] = useState(true); + + const { deleteSlo, success, loading, error } = useDeleteSlo(); + + if (loading) { + onDeleting(); + } + + if (success) { + toasts.addSuccess(getDeleteSuccesfulMessage(name)); + onDeleted(); + } + + if (error) { + toasts.addDanger(getDeleteFailMessage(name)); + } + + const handleConfirm = () => { + setIsVisible(false); + deleteSlo(id); + }; + + return isVisible ? ( + + {i18n.translate('xpack.observability.slos.slo.deleteConfirmationModal.descriptionText', { + defaultMessage: "You can't recover {name} after deleting.", + values: { name }, + })} + + ) : null; +} + +const getTitle = () => + i18n.translate('xpack.observability.slos.slo.deleteConfirmationModal.title', { + defaultMessage: 'Are you sure?', + }); + +const getCancelButtonText = () => + i18n.translate('xpack.observability.slos.slo.deleteConfirmationModal.cancelButtonLabel', { + defaultMessage: 'Cancel', + }); + +const getConfirmButtonText = (name: string) => + i18n.translate('xpack.observability.slos.slo.deleteConfirmationModal.deleteButtonLabel', { + defaultMessage: 'Delete {name}', + values: { name }, + }); + +const getDeleteSuccesfulMessage = (name: string) => + i18n.translate( + 'xpack.observability.slos.slo.deleteConfirmationModal.successNotification.descriptionText', + { + defaultMessage: 'Deleted {name}', + values: { name }, + } + ); + +const getDeleteFailMessage = (name: string) => + i18n.translate( + 'xpack.observability.slos.slo.deleteConfirmationModal.errorNotification.descriptionText', + { + defaultMessage: 'Failed to delete {name}', + values: { name }, + } + ); diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list.stories.tsx index eabf09ce9be26..3e6dd1c87d798 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list.stories.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list.stories.tsx @@ -8,12 +8,14 @@ import React from 'react'; import { ComponentStory } from '@storybook/react'; +import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator'; import { SloList as Component } from './slo_list'; export default { component: Component, title: 'app/SLO/ListPage/SloList', argTypes: {}, + decorators: [KibanaReactStorybookDecorator], }; const Template: ComponentStory = () => ; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx index 93f0f4d24e111..695124d464f05 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx @@ -5,17 +5,102 @@ * 2.0. */ -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useEffect, useMemo, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPagination } from '@elastic/eui'; +import { debounce } from 'lodash'; import { useFetchSloList } from '../../../hooks/slo/use_fetch_slo_list'; +import { SloListSearchFilterSortBar, SortItem, SortType } from './slo_list_search_filter_sort_bar'; +import { SloListItems } from './slo_list_items'; export function SloList() { - const { loading, sloList } = useFetchSloList(); + const [activePage, setActivePage] = useState(0); + + const [query, setQuery] = useState(''); + const [sort, setSort] = useState(); + const [filters, setFilters] = useState([]); + + const [deleting, setIsDeleting] = useState(false); + const [shouldReload, setShouldReload] = useState(false); + + const { + loading, + error, + sloList: { results: slos = [], total, perPage }, + } = useFetchSloList({ page: activePage + 1, name: query, refetch: shouldReload }); + + useEffect(() => { + if (shouldReload) { + setShouldReload(false); + setIsDeleting(false); + } + }, [shouldReload]); + + const handleDeleted = () => { + setShouldReload(true); + }; + + const handleDeleting = () => { + setIsDeleting(true); + }; + + const handlePageClick = (pageNumber: number) => { + setActivePage(pageNumber); + setShouldReload(true); + }; + + const handleChangeQuery = useMemo( + () => + debounce((e: React.ChangeEvent) => { + setQuery(e.target.value); + }, 300), + [] + ); + + const handleChangeSort = (newSort: SortType) => { + setSort(newSort); + }; + + const handleChangeFilter = (newFilters: SortItem[]) => { + setFilters(newFilters); + }; return ( - - {!loading &&
    {JSON.stringify(sloList, null, 2)}
    }
    + + + + + + + + + + {slos.length ? ( + + + + + + + + ) : null} ); } diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_empty.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_empty.stories.tsx new file mode 100644 index 0000000000000..b8c49050143fc --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_empty.stories.tsx @@ -0,0 +1,25 @@ +/* + * 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 from 'react'; +import { ComponentStory } from '@storybook/react'; + +import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator'; +import { SloListEmpty as Component } from './slo_list_empty'; + +export default { + component: Component, + title: 'app/SLO/ListPage/SloListEmpty', + decorators: [KibanaReactStorybookDecorator], +}; + +const Template: ComponentStory = () => ; + +const defaultProps = {}; + +export const SloListEmpty = Template.bind({}); +SloListEmpty.args = defaultProps; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_empty.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_empty.tsx new file mode 100644 index 0000000000000..eac54fdafec3d --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_empty.tsx @@ -0,0 +1,26 @@ +/* + * 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 from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function SloListEmpty() { + return ( + + {i18n.translate('xpack.observability.slos.list.emptyMessage', { + defaultMessage: 'There are no results for your criteria.', + })} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_error.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_error.stories.tsx new file mode 100644 index 0000000000000..a3e2503449da8 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_error.stories.tsx @@ -0,0 +1,25 @@ +/* + * 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 from 'react'; +import { ComponentStory } from '@storybook/react'; + +import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator'; +import { SloListError as Component } from './slo_list_error'; + +export default { + component: Component, + title: 'app/SLO/ListPage/SloListError', + decorators: [KibanaReactStorybookDecorator], +}; + +const Template: ComponentStory = () => ; + +const defaultProps = {}; + +export const SloListError = Template.bind({}); +SloListError.args = defaultProps; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_error.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_error.tsx new file mode 100644 index 0000000000000..47ae04b13c9ab --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_error.tsx @@ -0,0 +1,34 @@ +/* + * 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 from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function SloListError() { + return ( + + {i18n.translate('xpack.observability.slos.list.errorTitle', { + defaultMessage: 'Unable to load SLOs', + })} + + } + body={ +

    + {i18n.translate('xpack.observability.slos.list.errorMessage', { + defaultMessage: + 'There was an error loading the SLOs. Contact your administrator for help.', + })} +

    + } + /> + ); +} diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.stories.tsx new file mode 100644 index 0000000000000..2d996fb9c4c31 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.stories.tsx @@ -0,0 +1,30 @@ +/* + * 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 from 'react'; +import { ComponentStory } from '@storybook/react'; + +import { anSLO } from '../../../../common/data/slo'; +import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator'; +import { SloListItem as Component, SloListItemProps } from './slo_list_item'; + +export default { + component: Component, + title: 'app/SLO/ListPage/SloListItem', + decorators: [KibanaReactStorybookDecorator], +}; + +const Template: ComponentStory = (props: SloListItemProps) => ( + +); + +const defaultProps = { + slo: anSLO, +}; + +export const SloListItem = Template.bind({}); +SloListItem.args = defaultProps; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx new file mode 100644 index 0000000000000..34ae7f0a155d5 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx @@ -0,0 +1,141 @@ +/* + * 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, { useState } from 'react'; +import { + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiPanel, + EuiPopover, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { useKibana } from '../../../utils/kibana_react'; +import { SloSummaryStats } from './slo_summary_stats'; +import { SloDeleteConfirmationModal } from './slo_delete_confirmation_modal'; +import { SloBadges } from './slo_badges'; +import { paths } from '../../../config'; +import { SLO } from '../../../typings'; + +export interface SloListItemProps { + slo: SLO; + onDeleted: () => void; + onDeleting: () => void; +} + +export function SloListItem({ slo, onDeleted, onDeleting }: SloListItemProps) { + const { + application: { navigateToUrl }, + http: { basePath }, + } = useKibana().services; + + const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); + const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + + const handleClickActions = () => { + setIsActionsPopoverOpen(!isActionsPopoverOpen); + }; + + const handleDelete = () => { + setDeleteConfirmationModalOpen(true); + setIsDeleting(true); + setIsActionsPopoverOpen(false); + }; + + const handleNavigate = () => { + navigateToUrl(basePath.prepend(paths.observability.sloDetails(slo.id))); + }; + + const handleDeleteCancel = () => { + setDeleteConfirmationModalOpen(false); + setIsDeleting(false); + }; + + const handleDeleteSuccess = () => { + setDeleteConfirmationModalOpen(false); + onDeleted(); + }; + + return ( + + + {/* CONTENT */} + + + + + + {slo.name} + + + +
    + +
    +
    +
    +
    + + + + +
    +
    + + {/* ACTIONS */} + + + } + panelPaddingSize="none" + closePopover={handleClickActions} + isOpen={isActionsPopoverOpen} + > + + {i18n.translate('xpack.observability.slos.slo.item.actions.delete', { + defaultMessage: 'Delete', + })} + , + ]} + /> + + +
    + + {isDeleteConfirmationModalOpen ? ( + + ) : null} +
    + ); +} diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.stories.tsx new file mode 100644 index 0000000000000..7ad739b0afb84 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.stories.tsx @@ -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 React from 'react'; +import { ComponentStory } from '@storybook/react'; + +import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator'; +import { SloListItems as Component, SloListItemsProps } from './slo_list_items'; +import { anSLO } from '../../../../common/data/slo'; + +export default { + component: Component, + title: 'app/SLO/ListPage/SloListItems', + decorators: [KibanaReactStorybookDecorator], +}; + +const Template: ComponentStory = (props: SloListItemsProps) => ( + +); + +const defaultProps = { + slos: [anSLO, anSLO, anSLO], + loading: false, + error: false, + filters: [], + sort: undefined, + onDeleted: () => {}, + onDeleting: () => {}, +}; + +export const SloListItems = Template.bind({}); +SloListItems.args = defaultProps; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx new file mode 100644 index 0000000000000..13eb46041bbac --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_items.tsx @@ -0,0 +1,52 @@ +/* + * 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 from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { SloListItem } from './slo_list_item'; +import { SloListEmpty } from './slo_list_empty'; +import { sortSlos } from '../helpers/sort_slos'; +import { filterSlos } from '../helpers/filter_slos'; +import { SLO } from '../../../typings'; +import { SortItem, SortType } from './slo_list_search_filter_sort_bar'; +import { SloListError } from './slo_list_error'; + +export interface SloListItemsProps { + slos: SLO[]; + loading: boolean; + error: boolean; + filters: SortItem[]; + sort: SortType | undefined; + onDeleted: () => void; + onDeleting: () => void; +} + +export function SloListItems({ + slos, + loading, + error, + filters, + sort, + onDeleted, + onDeleting, +}: SloListItemsProps) { + return ( + + {slos.length + ? slos + .filter(filterSlos(filters)) + .sort(sortSlos(sort)) + .map((slo) => ( + + + + )) + : null} + {!loading && slos.length === 0 && !error ? : null} + {!loading && slos.length === 0 && error ? : null} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.stories.tsx new file mode 100644 index 0000000000000..feca705efd664 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.stories.tsx @@ -0,0 +1,35 @@ +/* + * 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 from 'react'; +import { ComponentStory } from '@storybook/react'; + +import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator'; +import { + SloListSearchFilterSortBar as Component, + SloListSearchFilterSortBarProps, +} from './slo_list_search_filter_sort_bar'; + +export default { + component: Component, + title: 'app/SLO/ListPage/SloListSearchFilterSortBar', + decorators: [KibanaReactStorybookDecorator], +}; + +const Template: ComponentStory = (props: SloListSearchFilterSortBarProps) => ( + +); + +const defaultProps = { + loading: false, + onChangeQuery: () => {}, + onChangeSort: () => {}, + onChangeStatusFilter: () => {}, +}; + +export const SloListSearchFilterSortBar = Template.bind({}); +SloListSearchFilterSortBar.args = defaultProps; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.tsx new file mode 100644 index 0000000000000..e138a2045f0bb --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_search_filter_sort_bar.tsx @@ -0,0 +1,192 @@ +/* + * 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 { + EuiFieldSearch, + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiPopoverTitle, + EuiSelectable, + EuiSelectableOption, +} from '@elastic/eui'; +import { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option'; +import { i18n } from '@kbn/i18n'; +import React, { useEffect, useState } from 'react'; + +export interface SloListSearchFilterSortBarProps { + loading: boolean; + onChangeQuery: (e: React.ChangeEvent) => void; + onChangeSort: (sortMethod: SortType) => void; + onChangeStatusFilter: (statusFilters: SortItem[]) => void; +} + +export type SortType = 'difference' | 'budgetRemaining'; +export type StatusType = 'violated' | 'forecastedViolation' | 'healthy'; + +export type SortItem = EuiSelectableOption & { + label: string; + type: SortType | StatusType; + checked?: EuiSelectableOptionCheckedType; +}; + +const SORT_OPTIONS: SortItem[] = [ + { + label: i18n.translate('xpack.observability.slos.list.sortBy.difference', { + defaultMessage: 'Difference', + }), + type: 'difference', + checked: 'on', + }, + { + label: i18n.translate('xpack.observability.slos.list.sortBy.budgetRemaining', { + defaultMessage: 'Budget remaining', + }), + type: 'budgetRemaining', + }, +]; + +const STATUS_OPTIONS: SortItem[] = [ + { + label: i18n.translate('xpack.observability.slos.list.statusFilter.violated', { + defaultMessage: 'Violated', + }), + type: 'violated', + }, + { + label: i18n.translate('xpack.observability.slos.list.statusFilter.healthy', { + defaultMessage: 'Healthy', + }), + type: 'healthy', + }, +]; + +export function SloListSearchFilterSortBar({ + loading, + onChangeQuery, + onChangeSort, + onChangeStatusFilter, +}: SloListSearchFilterSortBarProps) { + const [isFilterPopoverOpen, setFilterPopoverOpen] = useState(false); + const [isSortPopoverOpen, setSortPopoverOpen] = useState(false); + + const [sortOptions, setSortOptions] = useState(SORT_OPTIONS); + const [statusOptions, setStatusOptions] = useState(STATUS_OPTIONS); + + const selectedSort = sortOptions.find((option) => option.checked === 'on'); + const selectedStatusFilters = statusOptions.filter((option) => option.checked === 'on'); + + const handleToggleFilterButton = () => setFilterPopoverOpen(!isFilterPopoverOpen); + const handleToggleSortButton = () => setSortPopoverOpen(!isSortPopoverOpen); + + const handleChangeSort = (newOptions: SortItem[]) => { + setSortOptions(newOptions); + setSortPopoverOpen(false); + }; + + const handleChangeStatusOptions = (newOptions: SortItem[]) => { + setStatusOptions(newOptions); + setFilterPopoverOpen(false); + + onChangeStatusFilter(newOptions.filter((option) => option.checked === 'on')); + }; + + useEffect(() => { + if (selectedSort?.type === 'difference' || selectedSort?.type === 'budgetRemaining') { + onChangeSort(selectedSort.type); + } + }, [onChangeSort, selectedSort]); + + return ( + + + + + + + + + {i18n.translate('xpack.observability.slos.list.statusFilter', { + defaultMessage: 'Status', + })} + + } + isOpen={isFilterPopoverOpen} + closePopover={handleToggleFilterButton} + panelPaddingSize="none" + anchorPosition="downCenter" + > +
    + + {i18n.translate('xpack.observability.slos.list.statusFilter', { + defaultMessage: 'Status', + })} + + options={statusOptions} onChange={handleChangeStatusOptions}> + {(list) => list} + +
    +
    +
    +
    + + + + + {i18n.translate('xpack.observability.slos.list.sortByType', { + defaultMessage: 'Sort by {type}', + values: { type: selectedSort?.label || '' }, + })} + + } + isOpen={isSortPopoverOpen} + closePopover={handleToggleSortButton} + panelPaddingSize="none" + anchorPosition="downCenter" + > +
    + + {i18n.translate('xpack.observability.slos.list.sortBy', { + defaultMessage: 'Sort by', + })} + + + singleSelection + options={sortOptions} + onChange={handleChangeSort} + > + {(list) => list} + +
    +
    +
    +
    +
    + ); +} diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_welcome_prompt.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_welcome_prompt.tsx new file mode 100644 index 0000000000000..198a0a5ee4da8 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_welcome_prompt.tsx @@ -0,0 +1,80 @@ +/* + * 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 from 'react'; +import { EuiPageTemplate, EuiButton, EuiTitle, EuiLink, EuiImage } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import illustration from './assets/illustration.svg'; + +export function SloListWelcomePrompt() { + return ( + + +

    + {i18n.translate('xpack.observability.slos.sloList.welcomePrompt.title', { + defaultMessage: 'Track and deliver on your SLOs', + })} +

    + + } + icon={} + color="transparent" + layout="horizontal" + hasBorder={false} + body={ + <> +

    + {i18n.translate('xpack.observability.slos.sloList.welcomePrompt.messageParagraph1', { + defaultMessage: + 'Measure key metrics important to the business, such as service-level indicators and service-level objectives (SLIs/SLOs) to deliver on SLAs.', + })} +

    + +

    + {i18n.translate('xpack.observability.slos.sloList.welcomePrompt.messageParagraph2', { + defaultMessage: + 'Easily report the uptime and reliability of your services to stakeholders with real-time insights.', + })} +

    + +

    + {i18n.translate('xpack.observability.slos.sloList.welcomePrompt.messageParagraph3', { + defaultMessage: 'To get started, create your first SLO.', + })} +

    + + } + actions={ + + {i18n.translate('xpack.observability.slos.sloList.welcomePrompt.buttonLabel', { + defaultMessage: 'Create first SLO', + })} + + } + footer={ + <> + + + {i18n.translate('xpack.observability.slos.sloList.welcomePrompt.learnMore', { + defaultMessage: 'Want to learn more?', + })} + + +   + + {i18n.translate('xpack.observability.slos.sloList.welcomePrompt.learnMoreLink', { + defaultMessage: 'Read the docs', + })} + + + } + /> +
    + ); +} diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_summary_stats.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_summary_stats.stories.tsx new file mode 100644 index 0000000000000..b5fb58a279973 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_summary_stats.stories.tsx @@ -0,0 +1,29 @@ +/* + * 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 from 'react'; +import { ComponentStory } from '@storybook/react'; + +import { anSLO } from '../../../../common/data/slo'; +import { SloSummaryStats as Component, SloSummaryStatsProps } from './slo_summary_stats'; + +export default { + component: Component, + title: 'app/SLO/ListPage/SloSummaryStats', + argTypes: {}, +}; + +const Template: ComponentStory = (props: SloSummaryStatsProps) => ( + +); + +const defaultProps = { + slo: anSLO, +}; + +export const SloSummaryStats = Template.bind({}); +SloSummaryStats.args = defaultProps; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_summary_stats.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_summary_stats.tsx new file mode 100644 index 0000000000000..debb1ce29a3b1 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_summary_stats.tsx @@ -0,0 +1,78 @@ +/* + * 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 from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { asPercentWithTwoDecimals } from '../../../../common/utils/formatters'; +import { SLO } from '../../../typings'; +import { isSloHealthy } from '../helpers/is_slo_healthy'; +import { getSloDifference } from '../helpers/get_slo_difference'; + +export interface SloSummaryStatsProps { + slo: SLO; +} + +export function SloSummaryStats({ slo }: SloSummaryStatsProps) { + const isHealthy = isSloHealthy(slo); + const { label } = getSloDifference(slo); + + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability/public/pages/slos/helpers/filter_slos.ts b/x-pack/plugins/observability/public/pages/slos/helpers/filter_slos.ts new file mode 100644 index 0000000000000..69a2f7f558991 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/helpers/filter_slos.ts @@ -0,0 +1,25 @@ +/* + * 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 { SLO } from '../../../typings'; +import { SortItem } from '../components/slo_list_search_filter_sort_bar'; +import { isSloHealthy } from './is_slo_healthy'; + +export function filterSlos(filters: SortItem[]) { + return function (slo: SLO) { + return filters.length + ? filters.some((filter) => { + if (filter.type === 'violated') { + return !isSloHealthy(slo); + } + if (filter.type === 'healthy') { + return isSloHealthy(slo); + } + return false; + }) + : true; + }; +} diff --git a/x-pack/plugins/observability/public/pages/slos/helpers/get_slo_difference.ts b/x-pack/plugins/observability/public/pages/slos/helpers/get_slo_difference.ts new file mode 100644 index 0000000000000..4f78de5b5d90a --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/helpers/get_slo_difference.ts @@ -0,0 +1,17 @@ +/* + * 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 { asPercent } from '../../../../common/utils/formatters'; +import { SLO } from '../../../typings'; + +export function getSloDifference(slo: SLO) { + const difference = slo.summary.sliValue - slo.objective.target; + + return { + value: difference, + label: `${difference > 0 ? '+' : ''}${asPercent(difference, 1, 'n/a')}`, + }; +} diff --git a/x-pack/plugins/observability/public/pages/slos/helpers/is_slo_healthy.ts b/x-pack/plugins/observability/public/pages/slos/helpers/is_slo_healthy.ts new file mode 100644 index 0000000000000..b25fbc32820ae --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/helpers/is_slo_healthy.ts @@ -0,0 +1,11 @@ +/* + * 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 { SLO } from '../../../typings'; + +export function isSloHealthy(slo: SLO) { + return slo.objective.target <= slo.summary.sliValue; +} diff --git a/x-pack/plugins/observability/public/pages/slos/helpers/sort_slos.ts b/x-pack/plugins/observability/public/pages/slos/helpers/sort_slos.ts new file mode 100644 index 0000000000000..40c7243f88dcc --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/helpers/sort_slos.ts @@ -0,0 +1,23 @@ +/* + * 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 { SLO } from '../../../typings'; +import { SortType } from '../components/slo_list_search_filter_sort_bar'; +import { getSloDifference } from './get_slo_difference'; + +export function sortSlos(sort: SortType | undefined) { + return function (a: SLO, b: SLO) { + if (sort === 'difference') { + const { value: differenceA } = getSloDifference(a); + const { value: differenceB } = getSloDifference(b); + return differenceA - differenceB; + } + if (sort === 'budgetRemaining') { + return a.summary.errorBudget.remaining - b.summary.errorBudget.remaining; + } + return 0; + }; +} diff --git a/x-pack/plugins/observability/public/pages/slos/index.test.tsx b/x-pack/plugins/observability/public/pages/slos/index.test.tsx index 6f6c1349e9fbe..a06182ff53a2f 100644 --- a/x-pack/plugins/observability/public/pages/slos/index.test.tsx +++ b/x-pack/plugins/observability/public/pages/slos/index.test.tsx @@ -14,7 +14,7 @@ import { useKibana } from '../../utils/kibana_react'; import { kibanaStartMock } from '../../utils/kibana_react.mock'; import { render } from '../../utils/test_helper'; import { SlosPage } from '.'; -import { emptySloList } from '../../../common/data/slo'; +import { emptySloList, sloList } from '../../../common/data/slo'; import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list'; jest.mock('react-router-dom', () => ({ @@ -25,6 +25,8 @@ jest.mock('../../hooks/slo/use_fetch_slo_list'); jest.mock('../../utils/kibana_react'); jest.mock('../../hooks/use_breadcrumbs'); +jest.mock('./components/slo_list_item', () => ({ SloListItem: () => 'mocked SloListItem' })); + const useFetchSloListMock = useFetchSloList as jest.Mock; const useKibanaMock = useKibana as jest.Mock; const mockKibana = () => { @@ -50,19 +52,38 @@ describe('SLOs Page', () => { beforeEach(() => { jest.clearAllMocks(); mockKibana(); - useFetchSloListMock.mockReturnValue({ loading: false, sloList: emptySloList }); }); it('renders the not found page when the feature flag is not enabled', async () => { + useFetchSloListMock.mockReturnValue({ loading: false, sloList: emptySloList }); + render(, { unsafe: { slo: { enabled: false } } }); expect(screen.queryByTestId('pageNotFound')).toBeTruthy(); }); - it('renders the SLOs page when the feature flag is enabled', async () => { - render(, config); + describe('when the feature flag is enabled', () => { + it('renders nothing when the API is loading', async () => { + useFetchSloListMock.mockReturnValue({ loading: true, sloList: emptySloList }); + + const { container } = render(, config); + + expect(container).toBeEmptyDOMElement(); + }); + + it('renders the SLOs Welcome Prompt when the API has finished loading and there are no results', async () => { + useFetchSloListMock.mockReturnValue({ loading: false, sloList: emptySloList }); + render(, config); + + expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy(); + }); + + it('renders the SLOs page when the API has finished loading and there are results', async () => { + useFetchSloListMock.mockReturnValue({ loading: false, sloList }); + render(, config); - expect(screen.queryByTestId('slosPage')).toBeTruthy(); - expect(screen.queryByTestId('sloList')).toBeTruthy(); + expect(screen.queryByTestId('slosPage')).toBeTruthy(); + expect(screen.queryByTestId('sloList')).toBeTruthy(); + }); }); }); diff --git a/x-pack/plugins/observability/public/pages/slos/index.tsx b/x-pack/plugins/observability/public/pages/slos/index.tsx index 115da9d028e5d..a80f314ee59fa 100644 --- a/x-pack/plugins/observability/public/pages/slos/index.tsx +++ b/x-pack/plugins/observability/public/pages/slos/index.tsx @@ -13,14 +13,21 @@ import { usePluginContext } from '../../hooks/use_plugin_context'; import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; import { useKibana } from '../../utils/kibana_react'; import { isSloFeatureEnabled } from './helpers'; -import PageNotFound from '../404'; import { SLOS_BREADCRUMB_TEXT, SLOS_PAGE_TITLE } from './translations'; +import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list'; import { SloList } from './components/slo_list'; +import { SloListWelcomePrompt } from './components/slo_list_welcome_prompt'; +import PageNotFound from '../404'; export function SlosPage() { const { http } = useKibana().services; const { ObservabilityPageTemplate, config } = usePluginContext(); + const { + loading, + sloList: { total }, + } = useFetchSloList({ refetch: false }); + useBreadcrumbs([ { href: http.basePath.prepend(paths.observability.slos), @@ -32,12 +39,20 @@ export function SlosPage() { return ; } + if (loading) { + return null; + } + + if (total === 0) { + return ; + } + return ( {SLOS_PAGE_TITLE}, + pageTitle: SLOS_PAGE_TITLE, rightSideItems: [], - bottomBorder: true, + bottomBorder: false, }} data-test-subj="slosPage" > diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 8303a50f967c5..aa6abe01a9ec3 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -141,11 +141,20 @@ export class Plugin }, ], }, + { + id: 'slos', + title: i18n.translate('xpack.observability.slosLinkTitle', { + defaultMessage: 'SLOs', + }), + navLinkStatus: AppNavLinkStatus.hidden, + order: 8002, + path: '/slos', + }, getCasesDeepLinks({ basePath: casesPath, extend: { [CasesDeepLinkId.cases]: { - order: 8002, + order: 8003, navLinkStatus: AppNavLinkStatus.hidden, }, [CasesDeepLinkId.casesCreate]: { @@ -267,6 +276,7 @@ export class Plugin // See https://github.com/elastic/kibana/issues/103325. const otherLinks: NavigationEntry[] = deepLinks .filter((link) => link.navLinkStatus === AppNavLinkStatus.visible) + .filter((link) => (link.id === 'slos' ? config.unsafe.slo.enabled : link)) .map((link) => ({ app: observabilityAppId, label: link.title, diff --git a/x-pack/plugins/observability/public/update_global_navigation.test.tsx b/x-pack/plugins/observability/public/update_global_navigation.test.tsx index 9bd2f2319468c..344afcb239ef2 100644 --- a/x-pack/plugins/observability/public/update_global_navigation.test.tsx +++ b/x-pack/plugins/observability/public/update_global_navigation.test.tsx @@ -59,15 +59,17 @@ describe('updateGlobalNavigation', () => { [casesFeatureId]: { read_cases: true }, navLinks: { apm: true, logs: false, metrics: false, uptime: false }, } as unknown as ApplicationStart['capabilities']; - const deepLinks = [ - { - id: 'cases', - title: 'Cases', - order: 8002, - path: '/cases', - navLinkStatus: AppNavLinkStatus.hidden, - }, - ]; + + const caseRoute = { + id: 'cases', + title: 'Cases', + order: 8003, + path: '/cases', + navLinkStatus: AppNavLinkStatus.hidden, + }; + + const deepLinks = [caseRoute]; + const callback = jest.fn(); const updater$ = { next: (cb: AppUpdater) => callback(cb(app)), @@ -78,10 +80,7 @@ describe('updateGlobalNavigation', () => { expect(callback).toHaveBeenCalledWith({ deepLinks: [ { - id: 'cases', - title: 'Cases', - order: 8002, - path: '/cases', + ...caseRoute, navLinkStatus: AppNavLinkStatus.visible, }, ], @@ -96,15 +95,17 @@ describe('updateGlobalNavigation', () => { [casesFeatureId]: { read_cases: false }, navLinks: { apm: true, logs: false, metrics: false, uptime: false }, } as unknown as ApplicationStart['capabilities']; - const deepLinks = [ - { - id: 'cases', - title: 'Cases', - order: 8002, - path: '/cases', - navLinkStatus: AppNavLinkStatus.hidden, - }, - ]; + + const caseRoute = { + id: 'cases', + title: 'Cases', + order: 8003, + path: '/cases', + navLinkStatus: AppNavLinkStatus.hidden, + }; + + const deepLinks = [caseRoute]; + const callback = jest.fn(); const updater$ = { next: (cb: AppUpdater) => callback(cb(app)), @@ -115,10 +116,7 @@ describe('updateGlobalNavigation', () => { expect(callback).toHaveBeenCalledWith({ deepLinks: [ { - id: 'cases', - title: 'Cases', - order: 8002, - path: '/cases', + ...caseRoute, navLinkStatus: AppNavLinkStatus.hidden, }, ], @@ -164,5 +162,41 @@ describe('updateGlobalNavigation', () => { }); }); }); + + describe('when slos are enabled', () => { + it('shows the slos deep link', () => { + const capabilities = { + [casesFeatureId]: { read_cases: true }, + navLinks: { apm: true, logs: false, metrics: false, uptime: false }, + } as unknown as ApplicationStart['capabilities']; + + const sloRoute = { + id: 'slos', + title: 'SLOs', + order: 8002, + path: '/slos', + navLinkStatus: AppNavLinkStatus.hidden, + }; + + const deepLinks = [sloRoute]; + + const callback = jest.fn(); + const updater$ = { + next: (cb: AppUpdater) => callback(cb(app)), + } as unknown as Subject; + + updateGlobalNavigation({ capabilities, deepLinks, updater$ }); + + expect(callback).toHaveBeenCalledWith({ + deepLinks: [ + { + ...sloRoute, + navLinkStatus: AppNavLinkStatus.visible, + }, + ], + navLinkStatus: AppNavLinkStatus.visible, + }); + }); + }); }); }); diff --git a/x-pack/plugins/observability/public/update_global_navigation.tsx b/x-pack/plugins/observability/public/update_global_navigation.tsx index d4ebaaf350195..5a1bc87bbc753 100644 --- a/x-pack/plugins/observability/public/update_global_navigation.tsx +++ b/x-pack/plugins/observability/public/update_global_navigation.tsx @@ -37,6 +37,11 @@ export function updateGlobalNavigation({ ...link, navLinkStatus: someVisible ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden, }; + case 'slos': + return { + ...link, + navLinkStatus: someVisible ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden, + }; case 'rules': return { ...link, diff --git a/x-pack/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx b/x-pack/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx new file mode 100644 index 0000000000000..aa1cfbd6bea3c --- /dev/null +++ b/x-pack/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx @@ -0,0 +1,31 @@ +/* + * 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, { ComponentType } from 'react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; + +export function KibanaReactStorybookDecorator(Story: ComponentType) { + return ( + {} }, + http: { basePath: { prepend: (_: string) => '' } }, + docLinks: { links: { query: {} } }, + notifications: { toasts: {} }, + storage: { get: () => {} }, + uiSettings: { + get: (setting: string) => { + if (setting === 'dateFormat') { + return 'MMM D, YYYY @ HH:mm:ss.SSS'; + } + }, + }, + }} + > + + + ); +} diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts index 3a0e4046291e4..64f2e1b98f902 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts @@ -64,6 +64,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks.map((link) => link.text)).to.eql([ 'Overview', 'Alerts', + 'SLOs', 'APM', 'User Experience', 'Stack Management', @@ -119,6 +120,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks).to.eql([ 'Overview', 'Alerts', + 'SLOs', 'APM', 'User Experience', 'Stack Management', diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts index 3c9240ed4ff8d..799721c22bfb6 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts @@ -63,7 +63,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows infrastructure navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Alerts', 'Infrastructure', 'Stack Management']); + expect(navLinks).to.eql([ + 'Overview', + 'Alerts', + 'SLOs', + 'Infrastructure', + 'Stack Management', + ]); }); describe('infrastructure landing page without data', () => { @@ -161,7 +167,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows metrics navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Alerts', 'Infrastructure', 'Stack Management']); + expect(navLinks).to.eql([ + 'Overview', + 'Alerts', + 'SLOs', + 'Infrastructure', + 'Stack Management', + ]); }); describe('infrastructure landing page without data', () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts index 73e5a5b400884..1d946bc477404 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -60,7 +60,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Alerts', 'Logs', 'Stack Management']); + expect(navLinks).to.eql(['Overview', 'Alerts', 'SLOs', 'Logs', 'Stack Management']); }); describe('logs landing page without data', () => { @@ -123,7 +123,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Alerts', 'Logs', 'Stack Management']); + expect(navLinks).to.eql(['Overview', 'Alerts', 'SLOs', 'Logs', 'Stack Management']); }); describe('logs landing page without data', () => { diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts index fbe769796a9e0..5cf1804998aac 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts @@ -70,6 +70,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks.map((link) => link.text)).to.eql([ 'Overview', 'Alerts', + 'SLOs', 'Uptime', 'Synthetics', 'Stack Management', @@ -124,7 +125,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows uptime navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Alerts', 'Uptime', 'Synthetics', 'Stack Management']); + expect(navLinks).to.eql([ + 'Overview', + 'Alerts', + 'SLOs', + 'Uptime', + 'Synthetics', + 'Stack Management', + ]); }); it('can navigate to Uptime app', async () => { From ed1f9650d14f7af259fe11d771a0c6e1d64d6429 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Tue, 20 Dec 2022 10:00:30 +0100 Subject: [PATCH 41/55] [Security Solution] Add CellActions (alpha version) component to ui_actions plugin (#147434) ## Summary Create a `CellActions` component. It hooks into a UI-Actions trigger and displays all available actions. It has two modes, Hover_Actions and Always_Visible. You can run the storybook and take a look at the component: `yarn storybook ui_actions` or access https://ci-artifacts.kibana.dev/storybooks/pr-147434/226993c612bbe1719de6374219009bc69b0378d8/ui_actions/index.html *** This component is still not in use. Screenshot 2022-12-13 at 13 13 46 Screenshot 2022-12-13 at 13 13 30 #### Why? The security Solution team is creating a generic UI component to allow teams to share actions between different plugins. Initially, only the Security solution plugin will use this component and deprecate the Security solution custom implementation. Some actions that will be shared are: "copy to clipboard", "filter in", "filter out" and "add to timeline". #### How to use it: This package provides a uniform interface for displaying UI actions for a cell. For the `CellActions` component to work, it must be wrapped by `CellActionsContextProvider`. Ideally, the wrapper should stay on the top of the rendering tree. Example: ```JSX ... Hover me ``` `CellActions` component will display all compatible actions registered for the trigger id. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- .../steps/storybooks/build_and_upload.ts | 1 + src/dev/storybook/aliases.ts | 1 + src/plugins/ui_actions/.storybook/main.js | 17 ++ .../ui_actions/public/cell_actions/README.md | 17 ++ .../components/cell_action_item.test.tsx | 34 +++ .../components/cell_action_item.tsx | 45 ++++ .../components/cell_actions.stories.tsx | 77 +++++++ .../components/cell_actions.test.tsx | 74 +++++++ .../cell_actions/components/cell_actions.tsx | 140 +++++++++++++ .../components/cell_actions_context.test.tsx | 155 ++++++++++++++ .../components/cell_actions_context.tsx | 69 +++++++ .../components/extra_actions_button.test.tsx | 32 +++ .../components/extra_actions_button.tsx | 35 ++++ .../components/extra_actions_popover.test.tsx | 91 ++++++++ .../components/extra_actions_popover.tsx | 133 ++++++++++++ .../components/hover_actions_popover.test.tsx | 195 ++++++++++++++++++ .../components/hover_actions_popover.tsx | 168 +++++++++++++++ .../public/cell_actions/components/index.tsx | 10 + .../components/inline_actions.test.tsx | 63 ++++++ .../components/inline_actions.tsx | 62 ++++++ .../cell_actions/components/translations.ts | 29 +++ .../public/cell_actions/hooks/actions.test.ts | 85 ++++++++ .../public/cell_actions/hooks/actions.ts | 34 +++ .../public/cell_actions/mocks/helpers.ts | 21 ++ src/plugins/ui_actions/public/index.ts | 5 + 25 files changed, 1593 insertions(+) create mode 100644 src/plugins/ui_actions/.storybook/main.js create mode 100644 src/plugins/ui_actions/public/cell_actions/README.md create mode 100644 src/plugins/ui_actions/public/cell_actions/components/cell_action_item.test.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/cell_action_item.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/cell_actions.stories.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/cell_actions.test.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/cell_actions.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/cell_actions_context.test.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/cell_actions_context.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/extra_actions_button.test.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/extra_actions_button.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/extra_actions_popover.test.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/extra_actions_popover.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/hover_actions_popover.test.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/hover_actions_popover.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/index.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/inline_actions.test.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/inline_actions.tsx create mode 100644 src/plugins/ui_actions/public/cell_actions/components/translations.ts create mode 100644 src/plugins/ui_actions/public/cell_actions/hooks/actions.test.ts create mode 100644 src/plugins/ui_actions/public/cell_actions/hooks/actions.ts create mode 100644 src/plugins/ui_actions/public/cell_actions/mocks/helpers.ts diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.ts b/.buildkite/scripts/steps/storybooks/build_and_upload.ts index e315c7fbcceba..d15e15800821a 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.ts +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.ts @@ -41,6 +41,7 @@ const STORYBOOKS = [ 'security_solution', 'shared_ux', 'triggers_actions_ui', + 'ui_actions', 'ui_actions_enhanced', 'language_documentation_popover', 'unified_search', diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 7c37284bc3e77..05ae1c3048d17 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -45,5 +45,6 @@ export const storybookAliases = { threat_intelligence: 'x-pack/plugins/threat_intelligence/.storybook', triggers_actions_ui: 'x-pack/plugins/triggers_actions_ui/.storybook', ui_actions_enhanced: 'src/plugins/ui_actions_enhanced/.storybook', + ui_actions: 'src/plugins/ui_actions/.storybook', unified_search: 'src/plugins/unified_search/.storybook', }; diff --git a/src/plugins/ui_actions/.storybook/main.js b/src/plugins/ui_actions/.storybook/main.js new file mode 100644 index 0000000000000..0aaf1046299de --- /dev/null +++ b/src/plugins/ui_actions/.storybook/main.js @@ -0,0 +1,17 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const defaultConfig = require('@kbn/storybook').defaultConfig; + +module.exports = { + ...defaultConfig, + stories: ['../**/*.stories.tsx'], + reactOptions: { + strictMode: true, + }, +}; diff --git a/src/plugins/ui_actions/public/cell_actions/README.md b/src/plugins/ui_actions/public/cell_actions/README.md new file mode 100644 index 0000000000000..aca9ee7082ad5 --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/README.md @@ -0,0 +1,17 @@ +This package provides a uniform interface for displaying UI actions for a cell. +For the `CellActions` component to work, it must be wrapped by `CellActionsContextProvider`. Ideally, the wrapper should stay on the top of the rendering tree. + +Example: +```JSX + + ... + + Hover me + + + +``` + +`CellActions` component will display all compatible actions registered for the trigger id. \ No newline at end of file diff --git a/src/plugins/ui_actions/public/cell_actions/components/cell_action_item.test.tsx b/src/plugins/ui_actions/public/cell_actions/components/cell_action_item.test.tsx new file mode 100644 index 0000000000000..be2f57623419b --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/cell_action_item.test.tsx @@ -0,0 +1,34 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { makeAction } from '../mocks/helpers'; +import { CellActionExecutionContext } from './cell_actions'; +import { ActionItem } from './cell_action_item'; + +describe('ActionItem', () => { + it('renders', () => { + const action = makeAction('test-action'); + const actionContext = {} as CellActionExecutionContext; + const { queryByTestId } = render( + + ); + expect(queryByTestId('actionItem-test-action')).toBeInTheDocument(); + }); + + it('renders tooltip when showTooltip=true is received', () => { + const action = makeAction('test-action'); + const actionContext = {} as CellActionExecutionContext; + const { container } = render( + + ); + + expect(container.querySelector('.euiToolTipAnchor')).not.toBeNull(); + }); +}); diff --git a/src/plugins/ui_actions/public/cell_actions/components/cell_action_item.tsx b/src/plugins/ui_actions/public/cell_actions/components/cell_action_item.tsx new file mode 100644 index 0000000000000..1b1e57c04cbeb --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/cell_action_item.tsx @@ -0,0 +1,45 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useMemo } from 'react'; + +import { EuiButtonIcon, EuiToolTip, IconType } from '@elastic/eui'; +import type { Action } from '../../actions'; +import { CellActionExecutionContext } from './cell_actions'; + +export const ActionItem = ({ + action, + actionContext, + showTooltip, +}: { + action: Action; + actionContext: CellActionExecutionContext; + showTooltip: boolean; +}) => { + const actionProps = useMemo( + () => ({ + iconType: action.getIconType(actionContext) as IconType, + onClick: () => action.execute(actionContext), + 'data-test-subj': `actionItem-${action.id}`, + 'aria-label': action.getDisplayName(actionContext), + }), + [action, actionContext] + ); + + if (!actionProps.iconType) return null; + + return showTooltip ? ( + + + + ) : ( + + ); +}; diff --git a/src/plugins/ui_actions/public/cell_actions/components/cell_actions.stories.tsx b/src/plugins/ui_actions/public/cell_actions/components/cell_actions.stories.tsx new file mode 100644 index 0000000000000..4b3e4215bd266 --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/cell_actions.stories.tsx @@ -0,0 +1,77 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { ComponentStory } from '@storybook/react'; +import { CellActionsContextProvider } from './cell_actions_context'; +import { makeAction } from '../mocks/helpers'; +import { CellActions, CellActionsMode, CellActionsProps } from './cell_actions'; + +const TRIGGER_ID = 'testTriggerId'; + +const FIELD = { name: 'name', value: '123', type: 'text' }; + +const getCompatibleActions = () => + Promise.resolve([ + makeAction('Filter in', 'plusInCircle', 2), + makeAction('Filter out', 'minusInCircle', 3), + makeAction('Minimize', 'minimize', 1), + makeAction('Send email', 'email', 4), + makeAction('Pin field', 'pin', 5), + ]); + +export default { + title: 'CellAction', + decorators: [ + (storyFn: Function) => ( + +
    + {storyFn()} + + ), + ], +}; + +const CellActionsTemplate: ComponentStory> = (args) => ( + Field value +); + +export const DefaultWithControls = CellActionsTemplate.bind({}); + +DefaultWithControls.argTypes = { + mode: { + options: [CellActionsMode.HOVER_POPOVER, CellActionsMode.ALWAYS_VISIBLE], + defaultValue: CellActionsMode.HOVER_POPOVER, + control: { + type: 'radio', + }, + }, +}; + +DefaultWithControls.args = { + showActionTooltips: true, + mode: CellActionsMode.ALWAYS_VISIBLE, + triggerId: TRIGGER_ID, + field: FIELD, + visibleCellActions: 3, +}; + +export const CellActionInline = ({}: {}) => ( + + Field value + +); + +export const CellActionHoverPopup = ({}: {}) => ( + + Hover me + +); diff --git a/src/plugins/ui_actions/public/cell_actions/components/cell_actions.test.tsx b/src/plugins/ui_actions/public/cell_actions/components/cell_actions.test.tsx new file mode 100644 index 0000000000000..a9772643d24d8 --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/cell_actions.test.tsx @@ -0,0 +1,74 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act, render } from '@testing-library/react'; +import React from 'react'; +import { CellActions, CellActionsMode } from './cell_actions'; +import { CellActionsContextProvider } from './cell_actions_context'; + +const TRIGGER_ID = 'test-trigger-id'; +const FIELD = { name: 'name', value: '123', type: 'text' }; + +describe('CellActions', () => { + it('renders', async () => { + const getActionsPromise = Promise.resolve([]); + const getActions = () => getActionsPromise; + + const { queryByTestId } = render( + + + Field value + + + ); + + await act(async () => { + await getActionsPromise; + }); + + expect(queryByTestId('cellActions')).toBeInTheDocument(); + }); + + it('renders InlineActions when mode is ALWAYS_VISIBLE', async () => { + const getActionsPromise = Promise.resolve([]); + const getActions = () => getActionsPromise; + + const { queryByTestId } = render( + + + Field value + + + ); + + await act(async () => { + await getActionsPromise; + }); + + expect(queryByTestId('inlineActions')).toBeInTheDocument(); + }); + + it('renders HoverActionsPopover when mode is HOVER_POPOVER', async () => { + const getActionsPromise = Promise.resolve([]); + const getActions = () => getActionsPromise; + + const { queryByTestId } = render( + + + Field value + + + ); + + await act(async () => { + await getActionsPromise; + }); + + expect(queryByTestId('hoverActionsPopover')).toBeInTheDocument(); + }); +}); diff --git a/src/plugins/ui_actions/public/cell_actions/components/cell_actions.tsx b/src/plugins/ui_actions/public/cell_actions/components/cell_actions.tsx new file mode 100644 index 0000000000000..83076d30e9965 --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/cell_actions.tsx @@ -0,0 +1,140 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useMemo, useRef } from 'react'; +import type { ActionExecutionContext } from '../../actions'; +import { InlineActions } from './inline_actions'; +import { HoverActionsPopover } from './hover_actions_popover'; + +export interface CellActionField { + /** + * Field name. + * Example: 'host.name' + */ + name: string; + /** + * Field type. + * Example: 'keyword' + */ + type: string; + /** + * Field value. + * Example: 'My-Laptop' + */ + value: string; +} + +export interface CellActionExecutionContext extends ActionExecutionContext { + /** + * Ref to a DOM node where the action can add custom HTML. + */ + extraContentNodeRef: React.MutableRefObject; + + /** + * Ref to the node where the cell action are rendered. + */ + nodeRef: React.MutableRefObject; + + /** + * Extra configurations for actions. + */ + metadata?: Record; + + field: CellActionField; +} + +export enum CellActionsMode { + HOVER_POPOVER = 'hover-popover', + ALWAYS_VISIBLE = 'always-visible', +} + +export interface CellActionsProps { + /** + * Common set of properties used by most actions. + */ + field: CellActionField; + /** + * The trigger in which the actions are registered. + */ + triggerId: string; + /** + * UI configuration. Possible options are `HOVER_POPOVER` and `ALWAYS_VISIBLE`. + * + * `HOVER_POPOVER` shows the actions when the children component is hovered. + * + * `ALWAYS_VISIBLE` always shows the actions. + */ + mode: CellActionsMode; + + /** + * It displays a tooltip for every action button when `true`. + */ + showActionTooltips?: boolean; + /** + * It shows 'more actions' button when the number of actions is bigger than this parameter. + */ + visibleCellActions?: number; + /** + * Custom set of properties used by some actions. + * An action might require a specific set of metadata properties to render. + * This data is sent directly to actions. + */ + metadata?: Record; +} + +export const CellActions: React.FC = ({ + field, + triggerId, + children, + mode, + showActionTooltips = true, + visibleCellActions = 3, + metadata, +}) => { + const extraContentNodeRef = useRef(null); + const nodeRef = useRef(null); + + const actionContext: CellActionExecutionContext = useMemo( + () => ({ + field, + trigger: { id: triggerId }, + extraContentNodeRef, + nodeRef, + metadata, + }), + [field, triggerId, metadata] + ); + + if (mode === CellActionsMode.HOVER_POPOVER) { + return ( +
    + + {children} + + +
    +
    + ); + } + + return ( +
    + {children} + +
    +
    + ); +}; diff --git a/src/plugins/ui_actions/public/cell_actions/components/cell_actions_context.test.tsx b/src/plugins/ui_actions/public/cell_actions/components/cell_actions_context.test.tsx new file mode 100644 index 0000000000000..6ab425294bf71 --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/cell_actions_context.test.tsx @@ -0,0 +1,155 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { makeAction } from '../mocks/helpers'; +import { CellActionExecutionContext } from './cell_actions'; +import { + CellActionsContextProvider, + useLoadActions, + useLoadActionsFn, +} from './cell_actions_context'; + +describe('CellActionsContextProvider', () => { + const actionContext = { trigger: { id: 'triggerId' } } as CellActionExecutionContext; + + it('loads actions when useLoadActionsFn callback is called', async () => { + const action = makeAction('action-1', 'icon', 1); + const getActionsPromise = Promise.resolve([action]); + const getActions = () => getActionsPromise; + + const { result } = renderHook( + () => useLoadActionsFn(), + + { + wrapper: ({ children }) => ( + + {children} + + ), + } + ); + + const [{ value: valueBeforeFnCalled }, loadActions] = result.current; + + // value is undefined before loadActions is called + expect(valueBeforeFnCalled).toBeUndefined(); + + await act(async () => { + loadActions(actionContext); + await getActionsPromise; + }); + + const [{ value: valueAfterFnCalled }] = result.current; + + expect(valueAfterFnCalled).toEqual([action]); + }); + + it('loads actions when useLoadActions called', async () => { + const action = makeAction('action-1', 'icon', 1); + const getActionsPromise = Promise.resolve([action]); + const getActions = () => getActionsPromise; + + const { result } = renderHook( + () => useLoadActions(actionContext), + + { + wrapper: ({ children }) => ( + + {children} + + ), + } + ); + + await act(async () => { + await getActionsPromise; + }); + + expect(result.current.value).toEqual([action]); + }); + + it('sorts actions by order', async () => { + const firstAction = makeAction('action-1', 'icon', 1); + const secondAction = makeAction('action-2', 'icon', 2); + const getActionsPromise = Promise.resolve([secondAction, firstAction]); + const getActions = () => getActionsPromise; + + const { result } = renderHook( + () => useLoadActions(actionContext), + + { + wrapper: ({ children }) => ( + + {children} + + ), + } + ); + + await act(async () => { + await getActionsPromise; + }); + + expect(result.current.value).toEqual([firstAction, secondAction]); + }); + + it('sorts actions by id when order is undefined', async () => { + const firstAction = makeAction('action-1'); + const secondAction = makeAction('action-2'); + + const getActionsPromise = Promise.resolve([secondAction, firstAction]); + const getActions = () => getActionsPromise; + + const { result } = renderHook( + () => useLoadActions(actionContext), + + { + wrapper: ({ children }) => ( + + {children} + + ), + } + ); + + await act(async () => { + await getActionsPromise; + }); + + expect(result.current.value).toEqual([firstAction, secondAction]); + }); + + it('sorts actions by id and order', async () => { + const actionWithoutOrder = makeAction('action-1-no-order'); + const secondAction = makeAction('action-2', 'icon', 2); + const thirdAction = makeAction('action-3', 'icon', 3); + + const getActionsPromise = Promise.resolve([secondAction, actionWithoutOrder, thirdAction]); + const getActions = () => getActionsPromise; + + const { result } = renderHook( + () => useLoadActions(actionContext), + + { + wrapper: ({ children }) => ( + + {children} + + ), + } + ); + + await act(async () => { + await getActionsPromise; + }); + + expect(result.current.value).toEqual([secondAction, thirdAction, actionWithoutOrder]); + }); +}); diff --git a/src/plugins/ui_actions/public/cell_actions/components/cell_actions_context.tsx b/src/plugins/ui_actions/public/cell_actions/components/cell_actions_context.tsx new file mode 100644 index 0000000000000..8d1d2f0f709cf --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/cell_actions_context.tsx @@ -0,0 +1,69 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { orderBy } from 'lodash/fp'; +import React, { createContext, FC, useCallback, useContext } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import type { Action } from '../../actions'; +import { CellActionExecutionContext } from './cell_actions'; + +// It must to match `UiActionsService.getTriggerCompatibleActions` +type GetTriggerCompatibleActionsType = (triggerId: string, context: object) => Promise; + +type GetActionsType = (context: CellActionExecutionContext) => Promise; + +const CellActionsContext = createContext<{ getActions: GetActionsType } | null>(null); + +interface CellActionsContextProviderProps { + /** + * Please assign `uiActions.getTriggerCompatibleActions` function. + * This function should return a list of actions for a triggerId that are compatible with the provided context. + */ + getTriggerCompatibleActions: GetTriggerCompatibleActionsType; +} + +export const CellActionsContextProvider: FC = ({ + children, + getTriggerCompatibleActions, +}) => { + const getSortedCompatibleActions = useCallback( + (context) => + getTriggerCompatibleActions(context.trigger.id, context).then((actions) => + orderBy(['order', 'id'], ['asc', 'asc'], actions) + ), + [getTriggerCompatibleActions] + ); + + return ( + + {children} + + ); +}; + +const useCellActions = () => { + const context = useContext(CellActionsContext); + if (!context) { + throw new Error( + 'No CellActionsContext found. Please wrap the application with CellActionsContextProvider' + ); + } + + return context; +}; + +export const useLoadActions = (context: CellActionExecutionContext) => { + const { getActions } = useCellActions(); + return useAsync(() => getActions(context), []); +}; + +export const useLoadActionsFn = () => { + const { getActions } = useCellActions(); + return useAsyncFn(getActions, []); +}; diff --git a/src/plugins/ui_actions/public/cell_actions/components/extra_actions_button.test.tsx b/src/plugins/ui_actions/public/cell_actions/components/extra_actions_button.test.tsx new file mode 100644 index 0000000000000..0fcc81a9cc1c9 --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/extra_actions_button.test.tsx @@ -0,0 +1,32 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import { ExtraActionsButton } from './extra_actions_button'; + +describe('ExtraActionsButton', () => { + it('renders', () => { + const { queryByTestId } = render( {}} showTooltip={false} />); + + expect(queryByTestId('showExtraActionsButton')).toBeInTheDocument(); + }); + + it('renders tooltip when showTooltip=true is received', () => { + const { container } = render( {}} showTooltip />); + expect(container.querySelector('.euiToolTipAnchor')).not.toBeNull(); + }); + + it('calls onClick when button is clicked', () => { + const onClick = jest.fn(); + const { getByTestId } = render(); + + fireEvent.click(getByTestId('showExtraActionsButton')); + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/ui_actions/public/cell_actions/components/extra_actions_button.tsx b/src/plugins/ui_actions/public/cell_actions/components/extra_actions_button.tsx new file mode 100644 index 0000000000000..e70a28e5db4e3 --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/extra_actions_button.tsx @@ -0,0 +1,35 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import { SHOW_MORE_ACTIONS } from './translations'; + +interface ExtraActionsButtonProps { + onClick: () => void; + showTooltip: boolean; +} + +export const ExtraActionsButton: React.FC = ({ onClick, showTooltip }) => + showTooltip ? ( + + + + ) : ( + + ); diff --git a/src/plugins/ui_actions/public/cell_actions/components/extra_actions_popover.test.tsx b/src/plugins/ui_actions/public/cell_actions/components/extra_actions_popover.test.tsx new file mode 100644 index 0000000000000..07a0255d06231 --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/extra_actions_popover.test.tsx @@ -0,0 +1,91 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act, fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import { CellActionExecutionContext } from './cell_actions'; +import { makeAction } from '../mocks/helpers'; +import { ExtraActionsPopOver, ExtraActionsPopOverWithAnchor } from './extra_actions_popover'; + +const actionContext = { field: { name: 'fieldName' } } as CellActionExecutionContext; +describe('ExtraActionsPopOver', () => { + it('renders', () => { + const { queryByTestId } = render( + {}} + actions={[]} + button={} + /> + ); + + expect(queryByTestId('extraActionsPopOver')).toBeInTheDocument(); + }); + + it('executes action and close popover when menu item is clicked', async () => { + const executeAction = jest.fn(); + const closePopOver = jest.fn(); + const action = { ...makeAction('test-action'), execute: executeAction }; + const { getByLabelText } = render( + } + /> + ); + + await act(async () => { + await fireEvent.click(getByLabelText('test-action')); + }); + + expect(executeAction).toHaveBeenCalled(); + expect(closePopOver).toHaveBeenCalled(); + }); +}); + +describe('ExtraActionsPopOverWithAnchor', () => { + const anchorElement = document.createElement('span'); + document.body.appendChild(anchorElement); + + it('renders', () => { + const { queryByTestId } = render( + {}} + actions={[]} + anchorRef={{ current: anchorElement }} + /> + ); + + expect(queryByTestId('extraActionsPopOverWithAnchor')).toBeInTheDocument(); + }); + + it('executes action and close popover when menu item is clicked', () => { + const executeAction = jest.fn(); + const closePopOver = jest.fn(); + const action = { ...makeAction('test-action'), execute: executeAction }; + const { getByLabelText } = render( + + ); + + fireEvent.click(getByLabelText('test-action')); + + expect(executeAction).toHaveBeenCalled(); + expect(closePopOver).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/ui_actions/public/cell_actions/components/extra_actions_popover.tsx b/src/plugins/ui_actions/public/cell_actions/components/extra_actions_popover.tsx new file mode 100644 index 0000000000000..a4e12621f71e4 --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/extra_actions_popover.tsx @@ -0,0 +1,133 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + EuiContextMenuItem, + EuiContextMenuPanel, + EuiPopover, + EuiScreenReaderOnly, + EuiWrappingPopover, +} from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { css } from '@emotion/react'; +import type { Action } from '../../actions'; +import { EXTRA_ACTIONS_ARIA_LABEL, YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS } from './translations'; +import { CellActionExecutionContext } from './cell_actions'; + +const euiContextMenuItemCSS = css` + color: ${euiThemeVars.euiColorPrimaryText}; +`; + +interface ActionsPopOverProps { + actionContext: CellActionExecutionContext; + isOpen: boolean; + closePopOver: () => void; + actions: Action[]; + button: JSX.Element; +} + +export const ExtraActionsPopOver: React.FC = ({ + actions, + actionContext, + isOpen, + closePopOver, + button, +}) => ( + + + +); + +interface ExtraActionsPopOverWithAnchorProps + extends Pick { + anchorRef: React.RefObject; +} + +export const ExtraActionsPopOverWithAnchor = ({ + anchorRef, + actionContext, + isOpen, + closePopOver, + actions, +}: ExtraActionsPopOverWithAnchorProps) => { + return anchorRef.current ? ( + + + + ) : null; +}; + +type ExtraActionsPopOverContentProps = Pick< + ActionsPopOverProps, + 'actionContext' | 'closePopOver' | 'actions' +>; + +const ExtraActionsPopOverContent: React.FC = ({ + actionContext, + actions, + closePopOver, +}) => { + const items = useMemo( + () => + actions.map((action) => ( + { + closePopOver(); + action.execute(actionContext); + }} + > + {action.getDisplayName(actionContext)} + + )), + [actionContext, actions, closePopOver] + ); + return ( + <> + +

    {YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(actionContext.field.name)}

    +
    + + + ); +}; diff --git a/src/plugins/ui_actions/public/cell_actions/components/hover_actions_popover.test.tsx b/src/plugins/ui_actions/public/cell_actions/components/hover_actions_popover.test.tsx new file mode 100644 index 0000000000000..307d71e115299 --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/hover_actions_popover.test.tsx @@ -0,0 +1,195 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act, fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import { CellActionExecutionContext } from './cell_actions'; +import { makeAction } from '../mocks/helpers'; +import { HoverActionsPopover } from './hover_actions_popover'; +import { CellActionsContextProvider } from './cell_actions_context'; + +describe('HoverActionsPopover', () => { + const actionContext = { + trigger: { id: 'triggerId' }, + field: { name: 'fieldName' }, + } as CellActionExecutionContext; + const TestComponent = () => ; + jest.useFakeTimers(); + + it('renders', () => { + const getActions = () => Promise.resolve([]); + const { queryByTestId } = render( + + + + ); + expect(queryByTestId('hoverActionsPopover')).toBeInTheDocument(); + }); + + it('renders actions when hovered', async () => { + const action = makeAction('test-action'); + const getActionsPromise = Promise.resolve([action]); + const getActions = () => getActionsPromise; + + const { queryByLabelText, getByTestId } = render( + + + + + + ); + + await hoverElement(getByTestId('test-component'), async () => { + await getActionsPromise; + jest.runAllTimers(); + }); + + expect(queryByLabelText('test-action')).toBeInTheDocument(); + }); + + it('hide actions when mouse stops hovering', async () => { + const action = makeAction('test-action'); + const getActionsPromise = Promise.resolve([action]); + const getActions = () => getActionsPromise; + + const { queryByLabelText, getByTestId } = render( + + + + + + ); + + await hoverElement(getByTestId('test-component'), async () => { + await getActionsPromise; + jest.runAllTimers(); + }); + + // Mouse leaves hover state + await act(async () => { + fireEvent.mouseLeave(getByTestId('test-component')); + }); + + expect(queryByLabelText('test-action')).not.toBeInTheDocument(); + }); + + it('renders extra actions button', async () => { + const actions = [makeAction('test-action-1'), makeAction('test-action-2')]; + const getActionsPromise = Promise.resolve(actions); + const getActions = () => getActionsPromise; + + const { getByTestId } = render( + + + + + + ); + + await hoverElement(getByTestId('test-component'), async () => { + await getActionsPromise; + jest.runAllTimers(); + }); + + expect(getByTestId('showExtraActionsButton')).toBeInTheDocument(); + }); + + it('shows extra actions when extra actions button is clicked', async () => { + const actions = [makeAction('test-action-1'), makeAction('test-action-2')]; + const getActionsPromise = Promise.resolve(actions); + const getActions = () => getActionsPromise; + + const { getByTestId, getByLabelText } = render( + + + + + + ); + + await hoverElement(getByTestId('test-component'), async () => { + await getActionsPromise; + jest.runAllTimers(); + }); + + act(() => { + fireEvent.click(getByTestId('showExtraActionsButton')); + }); + + expect(getByLabelText('test-action-2')).toBeInTheDocument(); + }); + + it('does not render visible actions if extra actions are already rendered', async () => { + const actions = [ + makeAction('test-action-1'), + // extra actions + makeAction('test-action-2'), + makeAction('test-action-3'), + ]; + const getActionsPromise = Promise.resolve(actions); + const getActions = () => getActionsPromise; + + const { getByTestId, queryByLabelText } = render( + + + + + + ); + + await hoverElement(getByTestId('test-component'), async () => { + await getActionsPromise; + jest.runAllTimers(); + }); + + act(() => { + fireEvent.click(getByTestId('showExtraActionsButton')); + }); + + await hoverElement(getByTestId('test-component'), async () => { + await getActionsPromise; + jest.runAllTimers(); + }); + + expect(queryByLabelText('test-action-1')).not.toBeInTheDocument(); + expect(queryByLabelText('test-action-2')).toBeInTheDocument(); + expect(queryByLabelText('test-action-3')).toBeInTheDocument(); + }); +}); + +const hoverElement = async (element: Element, waitForChange: () => Promise) => { + await act(async () => { + fireEvent.mouseEnter(element); + await waitForChange(); + }); +}; diff --git a/src/plugins/ui_actions/public/cell_actions/components/hover_actions_popover.tsx b/src/plugins/ui_actions/public/cell_actions/components/hover_actions_popover.tsx new file mode 100644 index 0000000000000..b01db62172f1a --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/hover_actions_popover.tsx @@ -0,0 +1,168 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiPopover, EuiScreenReaderOnly } from '@elastic/eui'; + +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { css } from '@emotion/react'; +import { debounce } from 'lodash'; +import { ActionItem } from './cell_action_item'; +import { ExtraActionsButton } from './extra_actions_button'; +import { ACTIONS_AREA_LABEL, YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS } from './translations'; +import { partitionActions } from '../hooks/actions'; +import { ExtraActionsPopOverWithAnchor } from './extra_actions_popover'; +import { CellActionExecutionContext } from './cell_actions'; +import { useLoadActionsFn } from './cell_actions_context'; + +/** This class is added to the document body while dragging */ +export const IS_DRAGGING_CLASS_NAME = 'is-dragging'; + +// Overwrite Popover default minWidth to avoid displaying empty space +const PANEL_STYLE = { minWidth: `24px` }; + +const hoverContentWrapperCSS = css` + padding: 0 ${euiThemeVars.euiSizeS}; +`; + +/** + * To avoid expensive changes to the DOM, delay showing the popover menu + */ +const HOVER_INTENT_DELAY = 100; // ms + +interface Props { + children: React.ReactNode; + visibleCellActions: number; + actionContext: CellActionExecutionContext; + showActionTooltips: boolean; +} + +export const HoverActionsPopover = React.memo( + ({ children, visibleCellActions, actionContext, showActionTooltips }) => { + const contentRef = useRef(null); + const [isExtraActionsPopoverOpen, setIsExtraActionsPopoverOpen] = useState(false); + const [showHoverContent, setShowHoverContent] = useState(false); + const popoverRef = useRef(null); + + const [{ value: actions }, loadActions] = useLoadActionsFn(); + + const { visibleActions, extraActions } = useMemo( + () => partitionActions(actions ?? [], visibleCellActions), + [actions, visibleCellActions] + ); + + const closePopover = useCallback(() => { + setShowHoverContent(false); + }, []); + + const closeExtraActions = useCallback( + () => setIsExtraActionsPopoverOpen(false), + [setIsExtraActionsPopoverOpen] + ); + + const onShowExtraActionsClick = useCallback(() => { + setIsExtraActionsPopoverOpen(true); + closePopover(); + }, [closePopover, setIsExtraActionsPopoverOpen]); + + const openPopOverDebounced = useMemo( + () => + debounce(() => { + if (!document.body.classList.contains(IS_DRAGGING_CLASS_NAME)) { + setShowHoverContent(true); + } + }, HOVER_INTENT_DELAY), + [] + ); + + // prevent setState on an unMounted component + useEffect(() => { + return () => { + openPopOverDebounced.cancel(); + }; + }, [openPopOverDebounced]); + + const onMouseEnter = useCallback(async () => { + // Do not open actions with extra action popover is open + if (isExtraActionsPopoverOpen) return; + + // memoize actions after the first call + if (actions === undefined) { + loadActions(actionContext); + } + + openPopOverDebounced(); + }, [isExtraActionsPopoverOpen, actions, openPopOverDebounced, loadActions, actionContext]); + + const onMouseLeave = useCallback(() => { + closePopover(); + }, [closePopover]); + + const content = useMemo(() => { + return ( + // Hack - Forces extra actions popover to close when hover content is clicked. + // This hack is required because we anchor the popover to the hover content instead + // of anchoring it to the button that triggers the popover. + // eslint-disable-next-line jsx-a11y/click-events-have-key-events +
    + {children} +
    + ); + }, [onMouseEnter, closeExtraActions, children]); + + return ( + <> +
    + + {showHoverContent ? ( +
    + +

    {YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(actionContext.field.name)}

    +
    + {visibleActions.map((action) => ( + + ))} + {extraActions.length > 0 ? ( + + ) : null} +
    + ) : null} +
    +
    + + + ); + } +); diff --git a/src/plugins/ui_actions/public/cell_actions/components/index.tsx b/src/plugins/ui_actions/public/cell_actions/components/index.tsx new file mode 100644 index 0000000000000..fe75b51e9af3f --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/index.tsx @@ -0,0 +1,10 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { CellActions, CellActionsMode } from './cell_actions'; +export { CellActionsContextProvider } from './cell_actions_context'; diff --git a/src/plugins/ui_actions/public/cell_actions/components/inline_actions.test.tsx b/src/plugins/ui_actions/public/cell_actions/components/inline_actions.test.tsx new file mode 100644 index 0000000000000..d9147668b6b3f --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/inline_actions.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act, render } from '@testing-library/react'; +import React from 'react'; +import { CellActionExecutionContext } from './cell_actions'; +import { makeAction } from '../mocks/helpers'; +import { InlineActions } from './inline_actions'; +import { CellActionsContextProvider } from '.'; + +describe('InlineActions', () => { + const actionContext = { trigger: { id: 'triggerId' } } as CellActionExecutionContext; + it('renders', async () => { + const getActionsPromise = Promise.resolve([]); + const getActions = () => getActionsPromise; + const { queryByTestId } = render( + + + + ); + + await act(async () => { + await getActionsPromise; + }); + + expect(queryByTestId('inlineActions')).toBeInTheDocument(); + }); + + it('renders all actions', async () => { + const getActionsPromise = Promise.resolve([ + makeAction('action-1'), + makeAction('action-2'), + makeAction('action-3'), + makeAction('action-4'), + makeAction('action-5'), + ]); + const getActions = () => getActionsPromise; + const { queryAllByRole } = render( + + + + ); + + await act(async () => { + await getActionsPromise; + }); + + expect(queryAllByRole('button').length).toBe(5); + }); +}); diff --git a/src/plugins/ui_actions/public/cell_actions/components/inline_actions.tsx b/src/plugins/ui_actions/public/cell_actions/components/inline_actions.tsx new file mode 100644 index 0000000000000..0133b87e64392 --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/inline_actions.tsx @@ -0,0 +1,62 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { ActionItem } from './cell_action_item'; +import { usePartitionActions } from '../hooks/actions'; +import { ExtraActionsPopOver } from './extra_actions_popover'; +import { ExtraActionsButton } from './extra_actions_button'; +import { CellActionExecutionContext } from './cell_actions'; +import { useLoadActions } from './cell_actions_context'; + +interface InlineActionsProps { + actionContext: CellActionExecutionContext; + showActionTooltips: boolean; + visibleCellActions: number; +} + +export const InlineActions: React.FC = ({ + actionContext, + showActionTooltips, + visibleCellActions, +}) => { + const { value: allActions } = useLoadActions(actionContext); + const { extraActions, visibleActions } = usePartitionActions( + allActions ?? [], + visibleCellActions + ); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const togglePopOver = useCallback(() => setIsPopoverOpen((isOpen) => !isOpen), []); + const closePopOver = useCallback(() => setIsPopoverOpen(false), []); + const button = useMemo( + () => , + [togglePopOver, showActionTooltips] + ); + + return ( + + {visibleActions.map((action, index) => ( + + ))} + {extraActions.length > 0 ? ( + + ) : null} + + ); +}; diff --git a/src/plugins/ui_actions/public/cell_actions/components/translations.ts b/src/plugins/ui_actions/public/cell_actions/components/translations.ts new file mode 100644 index 0000000000000..272ebcb0cf334 --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/components/translations.ts @@ -0,0 +1,29 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { i18n } from '@kbn/i18n'; + +export const YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS = (fieldName: string) => + i18n.translate('uiActions.cellActions.youAreInADialogContainingOptionsScreenReaderOnly', { + values: { fieldName }, + defaultMessage: `You are in a dialog, containing options for field {fieldName}. Press tab to navigate options. Press escape to exit.`, + }); + +export const EXTRA_ACTIONS_ARIA_LABEL = i18n.translate( + 'uiActions.cellActions.extraActionsAriaLabel', + { + defaultMessage: 'Extra actions', + } +); + +export const SHOW_MORE_ACTIONS = i18n.translate('uiActions.showMoreActionsLabel', { + defaultMessage: 'More actions', +}); + +export const ACTIONS_AREA_LABEL = i18n.translate('uiActions.cellActions.actionsAriaLabel', { + defaultMessage: 'Actions', +}); diff --git a/src/plugins/ui_actions/public/cell_actions/hooks/actions.test.ts b/src/plugins/ui_actions/public/cell_actions/hooks/actions.test.ts new file mode 100644 index 0000000000000..a7ca564570e2c --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/hooks/actions.test.ts @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { makeAction } from '../mocks/helpers'; +import { partitionActions } from './actions'; + +describe('InlineActions', () => { + it('returns an empty array when actions is an empty array', async () => { + const { extraActions, visibleActions } = partitionActions([], 5); + + expect(visibleActions).toEqual([]); + expect(extraActions).toEqual([]); + }); + + it('returns only visible actions when visibleCellActions > actions.length', async () => { + const actions = [makeAction('action-1'), makeAction('action-2'), makeAction('action-3')]; + const { extraActions, visibleActions } = partitionActions(actions, 4); + + expect(visibleActions.length).toEqual(actions.length); + expect(extraActions).toEqual([]); + }); + + it('returns only extra actions when visibleCellActions is 1', async () => { + const actions = [makeAction('action-1'), makeAction('action-2'), makeAction('action-3')]; + const { extraActions, visibleActions } = partitionActions(actions, 1); + + expect(visibleActions).toEqual([]); + expect(extraActions.length).toEqual(actions.length); + }); + + it('returns only extra actions when visibleCellActions is 0', async () => { + const actions = [makeAction('action-1'), makeAction('action-2'), makeAction('action-3')]; + const { extraActions, visibleActions } = partitionActions(actions, 0); + + expect(visibleActions).toEqual([]); + expect(extraActions.length).toEqual(actions.length); + }); + + it('returns only extra actions when visibleCellActions is negative', async () => { + const actions = [makeAction('action-1'), makeAction('action-2'), makeAction('action-3')]; + const { extraActions, visibleActions } = partitionActions(actions, -6); + + expect(visibleActions).toEqual([]); + expect(extraActions.length).toEqual(actions.length); + }); + + it('returns only one visible action when visibleCellActionss 2 and action.length is 3', async () => { + const { extraActions, visibleActions } = partitionActions( + [makeAction('action-1'), makeAction('action-2'), makeAction('action-3')], + 2 + ); + + expect(visibleActions.length).toEqual(1); + expect(extraActions.length).toEqual(2); + }); + + it('returns two visible actions when visibleCellActions is 3 and action.length is 5', async () => { + const { extraActions, visibleActions } = partitionActions( + [ + makeAction('action-1'), + makeAction('action-2'), + makeAction('action-3'), + makeAction('action-4'), + makeAction('action-5'), + ], + 3 + ); + expect(visibleActions.length).toEqual(2); + expect(extraActions.length).toEqual(3); + }); + + it('returns three visible actions when visibleCellActions is 3 and action.length is 3', async () => { + const { extraActions, visibleActions } = partitionActions( + [makeAction('action-1'), makeAction('action-2'), makeAction('action-3')], + 3 + ); + expect(visibleActions.length).toEqual(3); + expect(extraActions.length).toEqual(0); + }); +}); diff --git a/src/plugins/ui_actions/public/cell_actions/hooks/actions.ts b/src/plugins/ui_actions/public/cell_actions/hooks/actions.ts new file mode 100644 index 0000000000000..84829a36d81bf --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/hooks/actions.ts @@ -0,0 +1,34 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useMemo } from 'react'; +import { Action } from '../../actions'; + +export const partitionActions = (actions: Action[], visibleCellActions: number) => { + if (visibleCellActions <= 1) return { extraActions: actions, visibleActions: [] }; + if (actions.length <= visibleCellActions) return { extraActions: [], visibleActions: actions }; + + return { + visibleActions: actions.slice(0, visibleCellActions - 1), + extraActions: actions.slice(visibleCellActions - 1, actions.length), + }; +}; + +export interface PartitionedActions { + extraActions: Array>; + visibleActions: Array>; +} + +export const usePartitionActions = ( + allActions: Action[], + visibleCellActions: number +): PartitionedActions => { + return useMemo(() => { + return partitionActions(allActions ?? [], visibleCellActions); + }, [allActions, visibleCellActions]); +}; diff --git a/src/plugins/ui_actions/public/cell_actions/mocks/helpers.ts b/src/plugins/ui_actions/public/cell_actions/mocks/helpers.ts new file mode 100644 index 0000000000000..c97b89ef505d8 --- /dev/null +++ b/src/plugins/ui_actions/public/cell_actions/mocks/helpers.ts @@ -0,0 +1,21 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const makeAction = (actionsName: string, icon: string = 'icon', order?: number) => ({ + id: actionsName, + type: actionsName, + order, + getIconType: () => icon, + getDisplayName: () => actionsName, + getDisplayNameTooltip: () => actionsName, + isCompatible: () => Promise.resolve(true), + execute: () => { + alert(actionsName); + return Promise.resolve(); + }, +}); diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index 29598b1c51b9b..8f6702e47dfb0 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -39,3 +39,8 @@ export { ACTION_VISUALIZE_LENS_FIELD, } from './types'; export type { ActionExecutionContext, ActionExecutionMeta, ActionMenuItemProps } from './actions'; +export { + CellActions, + CellActionsMode, + CellActionsContextProvider, +} from './cell_actions/components'; From 03bc7aecfd86de60ba71be57ef75180243db8ed8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 Dec 2022 04:22:19 -0500 Subject: [PATCH 42/55] Update dependency elastic-apm-node to ^3.41.0 (main) (#147814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [elastic-apm-node](https://togithub.com/elastic/apm-agent-nodejs) | [`^3.40.1` -> `^3.41.0`](https://renovatebot.com/diffs/npm/elastic-apm-node/3.40.1/3.41.0) | [![age](https://badges.renovateapi.com/packages/npm/elastic-apm-node/3.41.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/npm/elastic-apm-node/3.41.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/npm/elastic-apm-node/3.41.0/compatibility-slim/3.40.1)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/npm/elastic-apm-node/3.41.0/confidence-slim/3.40.1)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
    elastic/apm-agent-nodejs ### [`v3.41.0`](https://togithub.com/elastic/apm-agent-nodejs/releases/tag/v3.41.0) [Compare Source](https://togithub.com/elastic/apm-agent-nodejs/compare/v3.40.1...v3.41.0) For more information, please see the [changelog](https://www.elastic.co/guide/en/apm/agent/nodejs/current/release-notes-3.x.html#release-notes-3.41.0). ##### Elastic APM Node.js agent layer ARNs |Region|ARN| |------|---| |af-south-1|arn:aws:lambda:af-south-1:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |ap-east-1|arn:aws:lambda:ap-east-1:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |ap-northeast-1|arn:aws:lambda:ap-northeast-1:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |ap-northeast-2|arn:aws:lambda:ap-northeast-2:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |ap-northeast-3|arn:aws:lambda:ap-northeast-3:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |ap-south-1|arn:aws:lambda:ap-south-1:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |ap-southeast-1|arn:aws:lambda:ap-southeast-1:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |ap-southeast-2|arn:aws:lambda:ap-southeast-2:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |ap-southeast-3|arn:aws:lambda:ap-southeast-3:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |ca-central-1|arn:aws:lambda:ca-central-1:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |eu-central-1|arn:aws:lambda:eu-central-1:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |eu-north-1|arn:aws:lambda:eu-north-1:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |eu-south-1|arn:aws:lambda:eu-south-1:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |eu-west-1|arn:aws:lambda:eu-west-1:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |eu-west-2|arn:aws:lambda:eu-west-2:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |eu-west-3|arn:aws:lambda:eu-west-3:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |me-south-1|arn:aws:lambda:me-south-1:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |sa-east-1|arn:aws:lambda:sa-east-1:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |us-east-1|arn:aws:lambda:us-east-1:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |us-east-2|arn:aws:lambda:us-east-2:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |us-west-1|arn:aws:lambda:us-west-1:267093732750:layer:elastic-apm-node-ver-3-41-0:1| |us-west-2|arn:aws:lambda:us-west-2:267093732750:layer:elastic-apm-node-ver-3-41-0:1|
    --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/elastic/kibana). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f74c286cfc336..9087f045bcd17 100644 --- a/package.json +++ b/package.json @@ -512,7 +512,7 @@ "deepmerge": "^4.2.2", "del": "^6.1.0", "elastic-apm-http-client": "^11.0.1", - "elastic-apm-node": "^3.40.1", + "elastic-apm-node": "^3.41.0", "email-addresses": "^5.0.0", "execa": "^4.0.2", "expiry-js": "0.1.7", diff --git a/yarn.lock b/yarn.lock index d7a0ce8f49943..a6ccbd446c550 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12871,7 +12871,22 @@ elastic-apm-http-client@11.0.3, elastic-apm-http-client@^11.0.1: semver "^6.3.0" stream-chopper "^3.0.1" -elastic-apm-node@^3.38.0, elastic-apm-node@^3.40.1: +elastic-apm-http-client@11.0.4: + version "11.0.4" + resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-11.0.4.tgz#3e44e56fa42235b1b16a33c6a7656cfde595f9ff" + integrity sha512-449Qj/STi9hgnIk2KQ7719E7lpM3/i4Afs7NUhSOX8wV3sxn/+ItIHx9kKJthzhDDezxIfQcH83v83AF67GspQ== + dependencies: + agentkeepalive "^4.2.1" + breadth-filter "^2.0.0" + end-of-stream "^1.4.4" + fast-safe-stringify "^2.0.7" + fast-stream-to-buffer "^1.0.0" + object-filter-sequence "^1.0.0" + readable-stream "^3.4.0" + semver "^6.3.0" + stream-chopper "^3.0.1" + +elastic-apm-node@^3.38.0: version "3.40.1" resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.40.1.tgz#ae3669d480fdacf62ace40d12a6f1a3c46b37940" integrity sha512-vdyEZ7BPKJP2a1PkCsg350XXGZj03bwOiGrZdqgflocYxns5QwFbhvMKaVq7hWWWS8/sACesrLLELyQgdOpFsw== @@ -12909,6 +12924,44 @@ elastic-apm-node@^3.38.0, elastic-apm-node@^3.40.1: traverse "^0.6.6" unicode-byte-truncate "^1.0.0" +elastic-apm-node@^3.41.0: + version "3.41.0" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.41.0.tgz#ff0b5ecf6d126a2eb312ecc936bd55bc3f1e3ce6" + integrity sha512-cX745ryAwQCFEhkYttLrdjQDO0vCHspCbKg6bCguvBURZUHkLnZcP5z5m3R20dnGfTDXGg2q6nMi65iIVMCYFA== + dependencies: + "@elastic/ecs-pino-format" "^1.2.0" + "@opentelemetry/api" "^1.1.0" + after-all-results "^2.0.0" + async-cache "^1.1.0" + async-value-promise "^1.1.1" + basic-auth "^2.0.1" + cookie "^0.5.0" + core-util-is "^1.0.2" + elastic-apm-http-client "11.0.4" + end-of-stream "^1.4.4" + error-callsites "^2.0.4" + error-stack-parser "^2.0.6" + escape-string-regexp "^4.0.0" + fast-safe-stringify "^2.0.7" + http-headers "^3.0.2" + is-native "^1.0.1" + lru-cache "^6.0.0" + measured-reporting "^1.51.1" + monitor-event-loop-delay "^1.0.0" + object-filter-sequence "^1.0.0" + object-identity-map "^1.0.2" + original-url "^1.2.3" + pino "^6.11.2" + relative-microtime "^2.0.0" + require-in-the-middle "^5.2.0" + semver "^6.3.0" + set-cookie-serde "^1.0.0" + shallow-clone-shim "^2.0.0" + source-map "^0.8.0-beta.0" + sql-summary "^1.0.1" + traverse "^0.6.6" + unicode-byte-truncate "^1.0.0" + elasticsearch@^16.4.0: version "16.7.0" resolved "https://registry.yarnpkg.com/elasticsearch/-/elasticsearch-16.7.0.tgz#9055e3f586934d8de5fd407b04050e9d54173333" From 687987aa9ce56ce359f722485330179a4807d79a Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Tue, 20 Dec 2022 10:36:36 +0100 Subject: [PATCH 43/55] [Fleet] refactored bulk update tags retry (#147594) ## Summary Fixes https://github.com/elastic/kibana/issues/144161 As discussed [here](https://github.com/elastic/kibana/issues/144161#issuecomment-1348668610), the existing implementation of update tags doesn't work well with real agents, as there are many conflicts with checkin, even when trying to add/remove one tag. Refactored the logic to make retries more efficient: - Instead of aborting the whole bulk action on conflicts, changed the conflict strategy to 'proceed'. This means, if an action of 50k agents has 1k conflicts, not all 50k is retried, but only the 1k conflicts, this makes it less likely to conflict on retry. - Because of this, on retry we have to know which agents don't yet have the tag added/removed. For this, added an additional filter to the `updateByQuery` request. Only adding the filter if there is exactly one `tagsToAdd` or one `tagsToRemove`. This is the main use case from the UI, and handling other cases would complicate the logic more (each additional tag to add/remove would result in another OR query, which would match more agents, making conflicts more likely). - Added this additional query on the initial request as well (not only retries) to save on unnecessary work e.g. if the user tries to add a tag on 50k agents, but 48k already have it, it is enough to update the remaining 2k agents. - This improvement has the effect that 'Agent activity' shows the real updated agent count, not the total selected. I think this is not really a problem for update tags. - Cleaned up some of the UI logic, because the conflicts are fully handled now on the backend. - Locally I couldn't reproduce the conflict with agent checkins, even with 1k horde agents. I'll try to test in cloud with more real agents. To verify: - Enroll 50k agents (I used 50k with create_agents script, and 1k with horde). Enroll 50k with horde if possible. - Select all on UI and try to add/remove one or more tags - Expect the changes to propagate quickly (up to 1m). It might take a few refreshes to see the result on agent list and tags list, because the UI polls the agents every 30s. It is expected that the tags list temporarily shows incorrect data because the action is async. E.g. removed `test3` tag and added `add` tag quickly: image image The logs show the details of how many `version_conflicts` were there, and it decreased with retries. ``` [2022-12-15T10:32:12.937+01:00][INFO ][plugins.fleet] Running action asynchronously, actionId: 90acd541-19ac-4738-b3d3-db32789233de, total agents: 52000 [2022-12-15T10:32:12.981+01:00][INFO ][plugins.fleet] Scheduling task fleet:update_agent_tags:retry:check:90acd541-19ac-4738-b3d3-db32789233de [2022-12-15T10:32:16.477+01:00][INFO ][plugins.fleet] Running action asynchronously, actionId: 29e9da70-7194-4e52-8004-2c1b19f6dfd5, total agents: 52000 [2022-12-15T10:32:16.537+01:00][INFO ][plugins.fleet] Scheduling task fleet:update_agent_tags:retry:check:29e9da70-7194-4e52-8004-2c1b19f6dfd5 [2022-12-15T10:32:22.893+01:00][DEBUG][plugins.fleet] {"took":9886,"timed_out":false,"total":52000,"updated":41143,"deleted":0,"batches":52,"version_conflicts":10857,"noops":0,"retries":{"bulk":0,"search":0},"throttled_millis":0,"requests_per_second":-1,"throttled_until_millis":0,"failures":[]} [2022-12-15T10:32:26.066+01:00][DEBUG][plugins.fleet] {"took":9518,"timed_out":false,"total":52000,"updated":25755,"deleted":0,"batches":52,"version_conflicts":26245,"noops":0,"retries":{"bulk":0,"search":0},"throttled_millis":0,"requests_per_second":-1,"throttled_until_millis":0,"failures":[]} [2022-12-15T10:32:27.401+01:00][ERROR][plugins.fleet] Action failed: version conflict of 10857 agents [2022-12-15T10:32:27.461+01:00][INFO ][plugins.fleet] Scheduling task fleet:update_agent_tags:retry:90acd541-19ac-4738-b3d3-db32789233de [2022-12-15T10:32:27.462+01:00][INFO ][plugins.fleet] Retrying in task: fleet:update_agent_tags:retry:90acd541-19ac-4738-b3d3-db32789233de [2022-12-15T10:32:29.274+01:00][ERROR][plugins.fleet] Action failed: version conflict of 26245 agents [2022-12-15T10:32:29.353+01:00][INFO ][plugins.fleet] Scheduling task fleet:update_agent_tags:retry:29e9da70-7194-4e52-8004-2c1b19f6dfd5 [2022-12-15T10:32:29.353+01:00][INFO ][plugins.fleet] Retrying in task: fleet:update_agent_tags:retry:29e9da70-7194-4e52-8004-2c1b19f6dfd5 [2022-12-15T10:32:31.480+01:00][INFO ][plugins.fleet] Running bulk action retry task [2022-12-15T10:32:31.481+01:00][DEBUG][plugins.fleet] Retry #1 of task fleet:update_agent_tags:retry:90acd541-19ac-4738-b3d3-db32789233de [2022-12-15T10:32:31.481+01:00][INFO ][plugins.fleet] Running action asynchronously, actionId: 90acd541-19ac-4738-b3d3-db32789233de, total agents: 52000 [2022-12-15T10:32:31.481+01:00][INFO ][plugins.fleet] Completed bulk action retry task [2022-12-15T10:32:31.485+01:00][INFO ][plugins.fleet] Scheduling task fleet:update_agent_tags:retry:check:90acd541-19ac-4738-b3d3-db32789233de [2022-12-15T10:32:33.841+01:00][DEBUG][plugins.fleet] {"took":2347,"timed_out":false,"total":10857,"updated":9857,"deleted":0,"batches":11,"version_conflicts":1000,"noops":0,"retries":{"bulk":0,"search":0},"throttled_millis":0,"requests_per_second":-1,"throttled_until_millis":0,"failures":[]} [2022-12-15T10:32:34.556+01:00][INFO ][plugins.fleet] Running bulk action retry task [2022-12-15T10:32:34.557+01:00][DEBUG][plugins.fleet] Retry #1 of task fleet:update_agent_tags:retry:29e9da70-7194-4e52-8004-2c1b19f6dfd5 [2022-12-15T10:32:34.557+01:00][INFO ][plugins.fleet] Running action asynchronously, actionId: 29e9da70-7194-4e52-8004-2c1b19f6dfd5, total agents: 52000 [2022-12-15T10:32:34.557+01:00][INFO ][plugins.fleet] Completed bulk action retry task [2022-12-15T10:32:34.560+01:00][INFO ][plugins.fleet] Scheduling task fleet:update_agent_tags:retry:check:29e9da70-7194-4e52-8004-2c1b19f6dfd5 [2022-12-15T10:32:35.388+01:00][ERROR][plugins.fleet] Retry #1 of task fleet:update_agent_tags:retry:90acd541-19ac-4738-b3d3-db32789233de failed: version conflict of 1000 agents [2022-12-15T10:32:35.468+01:00][INFO ][plugins.fleet] Scheduling task fleet:update_agent_tags:retry:90acd541-19ac-4738-b3d3-db32789233de [2022-12-15T10:32:35.468+01:00][INFO ][plugins.fleet] Retrying in task: fleet:update_agent_tags:retry:90acd541-19ac-4738-b3d3-db32789233de {"took":5509,"timed_out":false,"total":26245,"updated":26245,"deleted":0,"batches":27,"version_conflicts":0,"noops":0,"retries":{"bulk":0,"search":0},"throttled_millis":0,"requests_per_second":-1,"throttled_until_millis":0,"failures":[]} [2022-12-15T10:32:42.722+01:00][INFO ][plugins.fleet] processed 26245 agents, took 5509ms [2022-12-15T10:32:42.723+01:00][INFO ][plugins.fleet] Removing task fleet:update_agent_tags:retry:check:29e9da70-7194-4e52-8004-2c1b19f6dfd5 [2022-12-15T10:32:46.705+01:00][INFO ][plugins.fleet] Running bulk action retry task [2022-12-15T10:32:46.706+01:00][DEBUG][plugins.fleet] Retry #2 of task fleet:update_agent_tags:retry:90acd541-19ac-4738-b3d3-db32789233de [2022-12-15T10:32:46.707+01:00][INFO ][plugins.fleet] Running action asynchronously, actionId: 90acd541-19ac-4738-b3d3-db32789233de, total agents: 52000 [2022-12-15T10:32:46.707+01:00][INFO ][plugins.fleet] Completed bulk action retry task [2022-12-15T10:32:46.711+01:00][INFO ][plugins.fleet] Scheduling task fleet:update_agent_tags:retry:check:90acd541-19ac-4738-b3d3-db32789233de [2022-12-15T10:32:47.099+01:00][DEBUG][plugins.fleet] {"took":379,"timed_out":false,"total":1000,"updated":1000,"deleted":0,"batches":1,"version_conflicts":0,"noops":0,"retries":{"bulk":0,"search":0},"throttled_millis":0,"requests_per_second":-1,"throttled_until_millis":0,"failures":[]} [2022-12-15T10:32:47.623+01:00][INFO ][plugins.fleet] processed 1000 agents, took 379ms [2022-12-15T10:32:47.623+01:00][INFO ][plugins.fleet] Removing task fleet:update_agent_tags:retry:check:90acd541-19ac-4738-b3d3-db32789233de ``` ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/tags_add_remove.test.tsx | 22 ++- .../components/tags_add_remove.tsx | 26 +-- .../server/services/agents/action_runner.ts | 6 +- .../server/services/agents/action_status.ts | 15 +- .../fleet/server/services/agents/crud.ts | 5 +- .../services/agents/update_agent_tags.test.ts | 153 +++++++++++++++++- .../services/agents/update_agent_tags.ts | 41 ++--- .../agents/update_agent_tags_action_runner.ts | 70 ++++---- .../apis/agents/update_agent_tags.ts | 91 ++++++----- 9 files changed, 284 insertions(+), 145 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx index 465db5236338c..4f4e7e24097f4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx @@ -292,7 +292,16 @@ describe('TagsAddRemove', () => { expect(mockBulkUpdateTags).toHaveBeenCalledWith( 'query', - ['newTag2', 'newTag'], + ['newTag'], + [], + expect.anything(), + 'Tag created', + 'Tag creation failed' + ); + + expect(mockBulkUpdateTags).toHaveBeenCalledWith( + 'query', + ['newTag2'], [], expect.anything(), 'Tag created', @@ -316,7 +325,16 @@ describe('TagsAddRemove', () => { expect(mockBulkUpdateTags).toHaveBeenCalledWith( '', [], - ['tag2', 'tag1'], + ['tag1'], + expect.anything(), + undefined, + undefined + ); + + expect(mockBulkUpdateTags).toHaveBeenCalledWith( + '', + [], + ['tag2'], expect.anything(), undefined, undefined diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx index 8307bc3467cc2..8e539954e5204 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx @@ -120,32 +120,10 @@ export const TagsAddRemove: React.FC = ({ errorMessage ); } else { - // sending updated tags to add/remove, in case multiple actions are done quickly and the first one is not yet propagated - const updatedTagsToAdd = tagsToAdd.concat( - labels - .filter( - (tag) => - tag.checked === 'on' && - !selectedTags.includes(tag.label) && - !tagsToRemove.includes(tag.label) - ) - .map((tag) => tag.label) - ); - const updatedTagsToRemove = tagsToRemove.concat( - labels - .filter( - (tag) => - tag.checked !== 'on' && - selectedTags.includes(tag.label) && - !tagsToAdd.includes(tag.label) - ) - .map((tag) => tag.label) - ); - updateTagsHook.bulkUpdateTags( agents!, - updatedTagsToAdd, - updatedTagsToRemove, + tagsToAdd, + tagsToRemove, (hasCompleted) => handleTagsUpdated(tagsToAdd, tagsToRemove, hasCompleted), successMessage, errorMessage diff --git a/x-pack/plugins/fleet/server/services/agents/action_runner.ts b/x-pack/plugins/fleet/server/services/agents/action_runner.ts index 18af331980238..83b61a340bfed 100644 --- a/x-pack/plugins/fleet/server/services/agents/action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/action_runner.ts @@ -22,6 +22,8 @@ import { getAgentActions } from './actions'; import { closePointInTime, getAgentsByKuery } from './crud'; import type { BulkActionsResolver } from './bulk_actions_resolver'; +export const MAX_RETRY_COUNT = 3; + export interface ActionParams { kuery: string; showInactive?: boolean; @@ -110,8 +112,8 @@ export abstract class ActionRunner { `Retry #${this.retryParams.retryCount} of task ${this.retryParams.taskId} failed: ${error.message}` ); - if (this.retryParams.retryCount === 3) { - const errorMessage = 'Stopping after 3rd retry. Error: ' + error.message; + if (this.retryParams.retryCount === MAX_RETRY_COUNT) { + const errorMessage = `Stopping after ${MAX_RETRY_COUNT}rd retry. Error: ${error.message}`; appContextService.getLogger().warn(errorMessage); // clean up tasks after 3rd retry reached diff --git a/x-pack/plugins/fleet/server/services/agents/action_status.ts b/x-pack/plugins/fleet/server/services/agents/action_status.ts index 5c6753425cbc7..c36e13a4441ca 100644 --- a/x-pack/plugins/fleet/server/services/agents/action_status.ts +++ b/x-pack/plugins/fleet/server/services/agents/action_status.ts @@ -69,12 +69,15 @@ export async function getActionStatuses( const nbAgentsActioned = action.nbAgentsActioned || action.nbAgentsActionCreated; const cardinalityCount = (matchingBucket?.agent_count as any)?.value ?? 0; const docCount = matchingBucket?.doc_count ?? 0; - const nbAgentsAck = Math.min( - docCount, - // only using cardinality count when count lower than precision threshold - docCount > PRECISION_THRESHOLD ? docCount : cardinalityCount, - nbAgentsActioned - ); + const nbAgentsAck = + action.type === 'UPDATE_TAGS' + ? Math.min(docCount, nbAgentsActioned) + : Math.min( + docCount, + // only using cardinality count when count lower than precision threshold + docCount > PRECISION_THRESHOLD ? docCount : cardinalityCount, + nbAgentsActioned + ); const completionTime = (matchingBucket?.max_timestamp as any)?.value_as_string; const complete = nbAgentsAck >= nbAgentsActioned; const cancelledAction = cancelledActions.find((a) => a.actionId === action.actionId); diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 97efef4f226c5..c3ad82518625f 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -155,7 +155,8 @@ export function getElasticsearchQuery( kuery: string, showInactive = false, includeHosted = false, - hostedPolicies: string[] = [] + hostedPolicies: string[] = [], + extraFilters: string[] = [] ): estypes.QueryDslQueryContainer | undefined { const filters = []; @@ -171,6 +172,8 @@ export function getElasticsearchQuery( filters.push('NOT (policy_id:{policyIds})'.replace('{policyIds}', hostedPolicies.join(','))); } + filters.push(...extraFilters); + const kueryNode = _joinFilters(filters); return kueryNode ? toElasticsearchQuery(kueryNode) : undefined; } diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts index a37165483f136..c357ed0e11edf 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts @@ -10,6 +10,7 @@ import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/serv import { createClientMock } from './action.mock'; import { updateAgentTags } from './update_agent_tags'; +import { updateTagsBatch } from './update_agent_tags_action_runner'; jest.mock('../app_context', () => { return { @@ -28,6 +29,7 @@ jest.mock('../agent_policy', () => { return { agentPolicyService: { getByIDs: jest.fn().mockResolvedValue([{ id: 'hosted-agent-policy', is_managed: true }]), + list: jest.fn().mockResolvedValue({ items: [] }), }, }; }); @@ -73,7 +75,7 @@ describe('update_agent_tags', () => { expect(esClient.updateByQuery).toHaveBeenCalledWith( expect.objectContaining({ - conflicts: 'abort', + conflicts: 'proceed', index: '.fleet-agents', query: { terms: { _id: ['agent1'] } }, script: expect.objectContaining({ @@ -90,6 +92,9 @@ describe('update_agent_tags', () => { }); it('should update action results on success', async () => { + esClient.updateByQuery.mockReset(); + esClient.updateByQuery.mockResolvedValue({ failures: [], updated: 1, total: 1 } as any); + await updateAgentTags(soClient, esClient, { agentIds: ['agent1'] }, ['one'], []); const agentAction = esClient.create.mock.calls[0][0] as any; @@ -110,11 +115,32 @@ describe('update_agent_tags', () => { expect(actionResults.body[1].error).not.toBeDefined(); }); - it('should write error action results for hosted agent when agentIds are passed', async () => { + it('should update action results on success - kuery', async () => { + await updateTagsBatch( + soClient, + esClient, + [], + {}, + { + tagsToAdd: ['new'], + tagsToRemove: [], + kuery: '', + } + ); + + const actionResults = esClient.bulk.mock.calls[0][0] as any; + const agentIds = actionResults?.body + ?.filter((i: any) => i.agent_id) + .map((i: any) => i.agent_id); + expect(agentIds[0]).toHaveLength(36); // uuid + expect(actionResults.body[1].error).not.toBeDefined(); + }); + + it('should skip hosted agent from total when agentIds are passed', async () => { const { esClient: esClientMock, agentInHostedDoc } = createClientMock(); esClientMock.updateByQuery.mockReset(); - esClientMock.updateByQuery.mockResolvedValue({ failures: [], updated: 0, total: '0' } as any); + esClientMock.updateByQuery.mockResolvedValue({ failures: [], updated: 0, total: 0 } as any); await updateAgentTags( soClient, @@ -130,13 +156,9 @@ describe('update_agent_tags', () => { action_id: expect.anything(), agents: [], type: 'UPDATE_TAGS', - total: 1, + total: 0, }) ); - - const errorResults = esClientMock.bulk.mock.calls[0][0] as any; - expect(errorResults.body[1].agent_id).toEqual(agentInHostedDoc._id); - expect(errorResults.body[1].error).toEqual('Cannot modify tags on a hosted agent'); }); it('should write error action results when failures are returned', async () => { @@ -152,6 +174,46 @@ describe('update_agent_tags', () => { expect(errorResults.body[1].error).toEqual('error reason'); }); + it('should throw error on version conflicts', async () => { + esClient.updateByQuery.mockReset(); + esClient.updateByQuery.mockResolvedValue({ + failures: [], + updated: 0, + version_conflicts: 100, + } as any); + + await expect( + updateAgentTags(soClient, esClient, { agentIds: ['agent1'] }, ['one'], []) + ).rejects.toThrowError('version conflict of 100 agents'); + }); + + it('should write out error results on last retry with version conflicts', async () => { + esClient.updateByQuery.mockReset(); + esClient.updateByQuery.mockResolvedValue({ + failures: [], + updated: 0, + version_conflicts: 100, + } as any); + + await expect( + updateTagsBatch( + soClient, + esClient, + [], + {}, + { + tagsToAdd: ['new'], + tagsToRemove: [], + kuery: '', + total: 100, + retryCount: 3, + } + ) + ).rejects.toThrowError('version conflict of 100 agents'); + const errorResults = esClient.bulk.mock.calls[0][0] as any; + expect(errorResults.body[1].error).toEqual('version conflict on 3rd retry'); + }); + it('should run add tags async when actioning more agents than batch size', async () => { esClient.search.mockResolvedValue({ hits: { @@ -180,4 +242,79 @@ describe('update_agent_tags', () => { expect(mockRunAsync).toHaveBeenCalled(); }); + + it('should add tags filter if only one tag to add', async () => { + await updateTagsBatch( + soClient, + esClient, + [], + {}, + { + tagsToAdd: ['new'], + tagsToRemove: [], + kuery: '', + } + ); + + const updateByQuery = esClient.updateByQuery.mock.calls[0][0] as any; + expect(updateByQuery.query).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ match: { active: true } }] } }, + { + bool: { + must_not: { bool: { minimum_should_match: 1, should: [{ match: { tags: 'new' } }] } }, + }, + }, + ], + }, + }); + }); + + it('should add tags filter if only one tag to remove', async () => { + await updateTagsBatch( + soClient, + esClient, + [], + {}, + { + tagsToAdd: [], + tagsToRemove: ['remove'], + kuery: '', + } + ); + + const updateByQuery = esClient.updateByQuery.mock.calls[0][0] as any; + expect(JSON.stringify(updateByQuery.query)).toContain( + '{"bool":{"should":[{"match":{"tags":"remove"}}],"minimum_should_match":1}}' + ); + }); + + it('should write total from updateByQuery result if query returns less results', async () => { + esClient.updateByQuery.mockReset(); + esClient.updateByQuery.mockResolvedValue({ failures: [], updated: 0, total: 50 } as any); + + await updateTagsBatch( + soClient, + esClient, + [], + {}, + { + tagsToAdd: ['new'], + tagsToRemove: [], + kuery: '', + total: 100, + } + ); + + const agentAction = esClient.create.mock.calls[0][0] as any; + expect(agentAction?.body).toEqual( + expect.objectContaining({ + action_id: expect.anything(), + agents: [], + type: 'UPDATE_TAGS', + total: 50, + }) + ); + }); }); diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts index 79636ecae1015..dad2053f7ed59 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts @@ -11,9 +11,7 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/ import type { Agent } from '../../types'; import { AgentReassignmentError } from '../../errors'; -import { SO_SEARCH_LIMIT } from '../../constants'; - -import { getAgentDocuments, getAgentsByKuery } from './crud'; +import { getAgentDocuments } from './crud'; import type { GetAgentsOptions } from '.'; import { searchHitToAgent } from './helpers'; import { UpdateAgentTagsActionRunner, updateTagsBatch } from './update_agent_tags_action_runner'; @@ -30,7 +28,7 @@ export async function updateAgentTags( tagsToRemove: string[] ): Promise<{ actionId: string }> { const outgoingErrors: Record = {}; - let givenAgents: Agent[] = []; + const givenAgents: Agent[] = []; if ('agentIds' in options) { const givenAgentsResults = await getAgentDocuments(esClient, options.agentIds); @@ -44,30 +42,17 @@ export async function updateAgentTags( } } } else if ('kuery' in options) { - const batchSize = options.batchSize ?? SO_SEARCH_LIMIT; - const res = await getAgentsByKuery(esClient, { - kuery: options.kuery, - showInactive: options.showInactive ?? false, - page: 1, - perPage: batchSize, - }); - if (res.total <= batchSize) { - givenAgents = res.agents; - } else { - return await new UpdateAgentTagsActionRunner( - esClient, - soClient, - { - ...options, - batchSize, - total: res.total, - kuery: options.kuery, - tagsToAdd, - tagsToRemove, - }, - { pitId: '' } - ).runActionAsyncWithRetry(); - } + return await new UpdateAgentTagsActionRunner( + esClient, + soClient, + { + ...options, + kuery: options.kuery, + tagsToAdd, + tagsToRemove, + }, + { pitId: '' } + ).runActionAsyncWithRetry(); } return await updateTagsBatch(soClient, esClient, givenAgents, outgoingErrors, { diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts index 042707b80df24..af538260bb163 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts @@ -20,7 +20,7 @@ import { agentPolicyService } from '../agent_policy'; import { SO_SEARCH_LIMIT } from '../../../common/constants'; -import { ActionRunner } from './action_runner'; +import { ActionRunner, MAX_RETRY_COUNT } from './action_runner'; import { BulkActionTaskType } from './bulk_actions_resolver'; import { filterHostedPolicies } from './filter_hosted_agents'; @@ -63,6 +63,7 @@ export class UpdateAgentTagsActionRunner extends ActionRunner { actionId: this.actionParams.actionId, total: this.actionParams.total, kuery: this.actionParams.kuery, + retryCount: this.retryParams.retryCount, } ); @@ -82,6 +83,7 @@ export async function updateTagsBatch( actionId?: string; total?: number; kuery?: string; + retryCount?: number; } ): Promise<{ actionId: string; updated?: number; took?: number }> { const errors: Record = { ...outgoingErrors }; @@ -94,11 +96,6 @@ export async function updateTagsBatch( hostedAgentError ); const agentIds = filteredAgents.map((agent) => agent.id); - const hostedAgentIds = givenAgents - .filter( - (agent) => filteredAgents.find((filteredAgent) => filteredAgent.id === agent.id) === undefined - ) - .map((agent) => agent.id); let query: estypes.QueryDslQueryContainer | undefined; if (options.kuery !== undefined) { @@ -108,7 +105,13 @@ export async function updateTagsBatch( }); const hostedIds = hostedPolicies.items.map((item) => item.id); - query = getElasticsearchQuery(options.kuery, false, false, hostedIds); + const extraFilters = []; + if (options.tagsToAdd.length === 1 && options.tagsToRemove.length === 0) { + extraFilters.push(`NOT (tags:${options.tagsToAdd[0]})`); + } else if (options.tagsToRemove.length === 1 && options.tagsToAdd.length === 0) { + extraFilters.push(`tags:${options.tagsToRemove[0]}`); + } + query = getElasticsearchQuery(options.kuery, false, false, hostedIds, extraFilters); } else { query = { terms: { @@ -150,7 +153,7 @@ export async function updateTagsBatch( updatedAt: new Date().toISOString(), }, }, - conflicts: 'abort', // relying on the task to retry in case of conflicts + conflicts: 'proceed', // relying on the task to retry in case of conflicts - retry only conflicted agents }); } catch (error) { throw new Error('Caught error: ' + JSON.stringify(error).slice(0, 1000)); @@ -159,27 +162,27 @@ export async function updateTagsBatch( appContextService.getLogger().debug(JSON.stringify(res).slice(0, 1000)); const actionId = options.actionId ?? uuid(); - const total = options.total ?? givenAgents.length; - // creating an action doc so that update tags shows up in activity - await createAgentAction(esClient, { - id: actionId, - agents: options.kuery === undefined ? agentIds : [], - created_at: new Date().toISOString(), - type: 'UPDATE_TAGS', - total, - }); + if (options.retryCount === undefined) { + // creating an action doc so that update tags shows up in activity + await createAgentAction(esClient, { + id: actionId, + agents: options.kuery === undefined ? agentIds : [], + created_at: new Date().toISOString(), + type: 'UPDATE_TAGS', + total: res.total, + }); + } - // creating unique 0...n ids to use as agentId, as we don't have all agent ids in case of action by kuery - const getArray = (count: number) => [...Array(count).keys()]; + // creating unique ids to use as agentId, as we don't have all agent ids in case of action by kuery + const getUuidArray = (count: number) => Array.from({ length: count }, () => uuid()); // writing successful action results if (res.updated ?? 0 > 0) { await bulkCreateAgentActionResults( esClient, - - (options.kuery === undefined ? agentIds : getArray(res.updated!)).map((id) => ({ - agentId: id + '', + (options.kuery === undefined ? agentIds : getUuidArray(res.updated!)).map((id) => ({ + agentId: id, actionId, })) ); @@ -197,18 +200,19 @@ export async function updateTagsBatch( ); } - // writing hosted agent errors - hosted agents filtered out - if ((res.total ?? total) < total) { - await bulkCreateAgentActionResults( - esClient, - (options.kuery === undefined ? hostedAgentIds : getArray(total - (res.total ?? total))).map( - (id) => ({ - agentId: id + '', + if (res.version_conflicts ?? 0 > 0) { + // write out error results on last retry, so action is not stuck in progress + if (options.retryCount === MAX_RETRY_COUNT) { + await bulkCreateAgentActionResults( + esClient, + getUuidArray(res.version_conflicts!).map((id) => ({ + agentId: id, actionId, - error: hostedAgentError, - }) - ) - ); + error: 'version conflict on 3rd retry', + })) + ); + } + throw new Error(`version conflict of ${res.version_conflicts} agents`); } return { actionId, updated: res.updated, took: res.took }; diff --git a/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts b/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts index ed3a147fe8ec7..05c1a1f8ae6c7 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts @@ -68,69 +68,78 @@ export default function (providerContext: FtrProviderContext) { expect(agent2data.body.item.tags).to.eql(['existingTag']); }); - it('should allow to update tags of multiple agents by kuery', async () => { - await supertest + async function pollResult( + actionId: string, + nbAgentsAck: number, + verifyActionResult: Function + ) { + await new Promise((resolve, reject) => { + let attempts = 0; + const intervalId = setInterval(async () => { + if (attempts > 4) { + clearInterval(intervalId); + reject('action timed out'); + } + ++attempts; + const { + body: { items: actionStatuses }, + } = await supertest.get(`/api/fleet/agents/action_status`).set('kbn-xsrf', 'xxx'); + const action = actionStatuses.find((a: any) => a.actionId === actionId); + if (action && action.nbAgentsAck === nbAgentsAck) { + clearInterval(intervalId); + await verifyActionResult(); + resolve({}); + } + }, 1000); + }).catch((e) => { + throw e; + }); + } + + it('should bulk update tags of multiple agents by kuery - add', async () => { + const { body: actionBody } = await supertest .post(`/api/fleet/agents/bulk_update_agent_tags`) .set('kbn-xsrf', 'xxx') .send({ agents: 'active: true', tagsToAdd: ['newTag'], - tagsToRemove: ['existingTag'], + tagsToRemove: [], }) .expect(200); - const { body } = await supertest.get(`/api/fleet/agents`).set('kbn-xsrf', 'xxx'); - expect(body.total).to.eql(4); - body.items.forEach((agent: any) => { - expect(agent.tags.includes('newTag')).to.be(true); - expect(agent.tags.includes('existingTag')).to.be(false); - }); + const actionId = actionBody.actionId; + + const verifyActionResult = async () => { + const { body } = await supertest + .get(`/api/fleet/agents?kuery=tags:newTag`) + .set('kbn-xsrf', 'xxx'); + expect(body.total).to.eql(4); + }; + + await pollResult(actionId, 4, verifyActionResult); }); - it('should bulk update tags of multiple agents by kuery in batches', async () => { + it('should bulk update tags of multiple agents by kuery - remove', async () => { const { body: actionBody } = await supertest .post(`/api/fleet/agents/bulk_update_agent_tags`) .set('kbn-xsrf', 'xxx') .send({ agents: 'active: true', - tagsToAdd: ['newTag'], + tagsToAdd: [], tagsToRemove: ['existingTag'], - batchSize: 3, }) .expect(200); const actionId = actionBody.actionId; const verifyActionResult = async () => { - const { body } = await supertest.get(`/api/fleet/agents`).set('kbn-xsrf', 'xxx'); - expect(body.total).to.eql(4); - body.items.forEach((agent: any) => { - expect(agent.tags.includes('newTag')).to.be(true); - expect(agent.tags.includes('existingTag')).to.be(false); - }); + const { body } = await supertest + .get(`/api/fleet/agents?kuery=tags:existingTag`) + .set('kbn-xsrf', 'xxx'); + expect(body.total).to.eql(0); }; - await new Promise((resolve, reject) => { - let attempts = 0; - const intervalId = setInterval(async () => { - if (attempts > 4) { - clearInterval(intervalId); - reject('action timed out'); - } - ++attempts; - const { - body: { items: actionStatuses }, - } = await supertest.get(`/api/fleet/agents/action_status`).set('kbn-xsrf', 'xxx'); - const action = actionStatuses.find((a: any) => a.actionId === actionId); - if (action && action.nbAgentsAck === 4) { - clearInterval(intervalId); - await verifyActionResult(); - resolve({}); - } - }, 1000); - }).catch((e) => { - throw e; - }); + await pollResult(actionId, 2, verifyActionResult); }); it('should return a 403 if user lacks fleet all permissions', async () => { @@ -180,8 +189,8 @@ export default function (providerContext: FtrProviderContext) { .get(`/api/fleet/agents/action_status`) .set('kbn-xsrf', 'xxx'); const actionStatus = body.items[0]; - expect(actionStatus.status).to.eql('FAILED'); - expect(actionStatus.nbAgentsFailed).to.eql(1); + expect(actionStatus.status).to.eql('COMPLETE'); + expect(actionStatus.nbAgentsAck).to.eql(1); }); }); }); From 612b16bffa414350e372bf606c04bd4e7097163d Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 20 Dec 2022 10:38:03 +0100 Subject: [PATCH 44/55] [ML] Update imports to use @kbn/field-types package (#147769) Where applicable, update imports to use `@kbn/field-types` package instead of `@kbn/data-plugin/common` plugin. Should we move some of our own code to packages, the import via the plugin wouldn't be allowed. --- x-pack/plugins/aiops/public/application/utils/search_utils.ts | 3 +-- .../full_time_range_selector/full_time_range_selector.tsx | 2 +- x-pack/plugins/data_visualizer/common/constants.ts | 2 +- .../common/components/fields_stats_grid/get_field_names.ts | 2 +- .../application/common/components/top_values/top_values.tsx | 2 +- .../full_time_range_selector/full_time_range_selector.tsx | 2 +- .../hooks/use_data_visualizer_grid_data.ts | 3 ++- .../application/index_data_visualizer/locator/locator.ts | 2 +- .../search_strategy/requests/get_numeric_field_stats.ts | 2 +- .../search_strategy/requests/overall_stats.ts | 2 +- x-pack/plugins/ml/common/types/fields.ts | 2 +- x-pack/plugins/ml/common/util/fields_utils.ts | 2 +- .../ml/public/application/components/data_grid/common.ts | 2 +- .../components/data_grid/use_column_chart.test.tsx | 2 +- .../application/components/data_grid/use_column_chart.tsx | 2 +- .../public/application/data_frame_analytics/common/fields.ts | 2 +- .../data_frame_analytics/common/get_index_fields.ts | 2 +- .../components/configuration_step/form_options_validation.ts | 2 +- .../configuration_step/supported_fields_message.tsx | 2 +- .../application/datavisualizer/index_based/common/request.ts | 2 +- .../plugins/ml/public/application/explorer/explorer_utils.ts | 2 +- .../jobs/new_job/common/job_creator/job_creator.ts | 2 +- .../jobs/new_job/common/job_creator/util/general.ts | 2 +- .../ml/public/application/jobs/new_job/job_from_lens/utils.ts | 2 +- .../public/application/jobs/new_job/pages/job_type/page.tsx | 2 +- .../services/new_job_capabilities/new_job_capabilities.ts | 2 +- .../new_job_capabilities/new_job_capabilities_service.ts | 2 +- .../new_job_capabilities_service_analytics.ts | 2 +- .../ml/public/application/util/field_types_utils.test.ts | 4 ++-- .../plugins/ml/public/application/util/field_types_utils.ts | 4 ++-- .../server/models/job_service/new_job_caps/field_service.ts | 2 +- .../ml/server/models/job_validation/validate_time_range.ts | 2 +- x-pack/plugins/transform/common/api_schemas/transforms.ts | 2 +- x-pack/plugins/transform/public/app/common/pivot_aggs.ts | 2 +- x-pack/plugins/transform/public/app/common/pivot_group_by.ts | 2 +- .../plugins/transform/public/app/hooks/__mocks__/use_api.ts | 2 +- x-pack/plugins/transform/public/app/hooks/use_api.ts | 2 +- .../plugins/transform/public/app/hooks/use_pivot_data.test.ts | 2 +- x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts | 2 +- .../common/filter_agg/components/filter_agg_form.test.tsx | 2 +- .../components/step_define/common/filter_agg/constants.ts | 2 +- .../step_define/common/get_pivot_dropdown_options.ts | 2 +- .../create_transform/components/step_define/common/types.ts | 2 +- .../step_define/hooks/use_latest_function_config.ts | 2 +- .../components/step_define/hooks/use_pivot_config.ts | 2 +- .../components/step_details/step_details_form.tsx | 2 +- .../edit_transform_flyout/edit_transform_flyout_form.tsx | 2 +- 47 files changed, 50 insertions(+), 50 deletions(-) diff --git a/x-pack/plugins/aiops/public/application/utils/search_utils.ts b/x-pack/plugins/aiops/public/application/utils/search_utils.ts index d3643daace942..3bd0ea7a96523 100644 --- a/x-pack/plugins/aiops/public/application/utils/search_utils.ts +++ b/x-pack/plugins/aiops/public/application/utils/search_utils.ts @@ -10,7 +10,7 @@ import { cloneDeep } from 'lodash'; import { IUiSettingsClient } from '@kbn/core/public'; -import { SearchSource } from '@kbn/data-plugin/common'; +import { getEsQueryConfig, SearchSource } from '@kbn/data-plugin/common'; import { SavedSearch } from '@kbn/discover-plugin/public'; import { FilterManager } from '@kbn/data-plugin/public'; import { @@ -23,7 +23,6 @@ import { } from '@kbn/es-query'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { DataView } from '@kbn/data-views-plugin/public'; -import { getEsQueryConfig } from '@kbn/data-plugin/common'; import type { SimpleSavedObject } from '@kbn/core/public'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; diff --git a/x-pack/plugins/aiops/public/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/plugins/aiops/public/components/full_time_range_selector/full_time_range_selector.tsx index 24d3877143a7e..592240675197c 100644 --- a/x-pack/plugins/aiops/public/components/full_time_range_selector/full_time_range_selector.tsx +++ b/x-pack/plugins/aiops/public/components/full_time_range_selector/full_time_range_selector.tsx @@ -12,7 +12,7 @@ import React, { FC, useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { TimefilterContract } from '@kbn/data-plugin/public'; -import { DataView } from '@kbn/data-plugin/common'; +import type { DataView } from '@kbn/data-plugin/common'; import { EuiButton, EuiButtonIcon, diff --git a/x-pack/plugins/data_visualizer/common/constants.ts b/x-pack/plugins/data_visualizer/common/constants.ts index f56fd88566c30..c5aa71967f615 100644 --- a/x-pack/plugins/data_visualizer/common/constants.ts +++ b/x-pack/plugins/data_visualizer/common/constants.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import type { DocLinksStart } from '@kbn/core/public'; export const APP_ID = 'data_visualizer'; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/get_field_names.ts b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/get_field_names.ts index c3026496cf5c8..406379fca7340 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/get_field_names.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/fields_stats_grid/get_field_names.ts @@ -6,7 +6,7 @@ */ import { difference } from 'lodash'; -import { ES_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import type { FindFileStructureResponse } from '@kbn/file-upload-plugin/common'; import type { SupportedFieldType } from '../../../../../common/types'; import { SUPPORTED_FIELD_TYPES } from '../../../../../common/constants'; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx index dec10fb528422..b007e9883beaf 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/top_values/top_values.tsx @@ -20,7 +20,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import classNames from 'classnames'; import { i18n } from '@kbn/i18n'; import { DataViewField } from '@kbn/data-views-plugin/public'; -import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; import { css } from '@emotion/react'; import { useDataVisualizerKibana } from '../../../kibana_context'; import { roundToDecimalPlace, kibanaFieldFormat } from '../utils'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx index aeebdcd6b89f6..1cade1deaa59a 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx @@ -8,7 +8,7 @@ import React, { FC, useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { TimefilterContract } from '@kbn/data-plugin/public'; -import { DataView } from '@kbn/data-plugin/common'; +import type { DataView } from '@kbn/data-plugin/common'; import { EuiButton, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts index 30bd9b56a7562..9bb8943d56c73 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_data_visualizer_grid_data.ts @@ -10,7 +10,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { merge } from 'rxjs'; import type { EuiTableActionsColumnType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DataViewField, KBN_FIELD_TYPES, UI_SETTINGS } from '@kbn/data-plugin/common'; +import { type DataViewField, UI_SETTINGS } from '@kbn/data-plugin/common'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import seedrandom from 'seedrandom'; import type { SamplingOption } from '@kbn/discover-plugin/public/application/main/components/field_stats_table/field_stats_table'; import type { RandomSamplerOption } from '../constants/random_sampler'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts index 0d1b90aa0a124..ef93159b795c7 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/locator/locator.ts @@ -8,7 +8,7 @@ import { encode } from '@kbn/rison'; import { stringify } from 'query-string'; import { SerializableRecord } from '@kbn/utility-types'; import { Filter, TimeRange } from '@kbn/es-query'; -import { RefreshInterval } from '@kbn/data-plugin/common'; +import type { RefreshInterval } from '@kbn/data-plugin/common'; import { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/common'; import { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public'; import { Dictionary, isRisonSerializationRequired } from '../../common/util/url_state'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts index 7e016d40cdd39..66a7134919760 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts @@ -10,7 +10,7 @@ import { find, get } from 'lodash'; import { catchError, map } from 'rxjs/operators'; import { Observable, of } from 'rxjs'; import { AggregationsTermsAggregation } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { +import type { IKibanaSearchRequest, IKibanaSearchResponse, ISearchOptions, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/overall_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/overall_stats.ts index 7c0c4ccb9c498..28bf46bfd7c05 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/overall_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/overall_stats.ts @@ -8,7 +8,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { get } from 'lodash'; import { Query } from '@kbn/es-query'; -import { IKibanaSearchResponse } from '@kbn/data-plugin/common'; +import type { IKibanaSearchResponse } from '@kbn/data-plugin/common'; import type { AggCardinality } from '@kbn/ml-agg-utils'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { buildAggregationWithSamplingOption } from './build_random_sampler_agg'; diff --git a/x-pack/plugins/ml/common/types/fields.ts b/x-pack/plugins/ml/common/types/fields.ts index 3134d470f889f..7dea8c1386aca 100644 --- a/x-pack/plugins/ml/common/types/fields.ts +++ b/x-pack/plugins/ml/common/types/fields.ts @@ -6,7 +6,7 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ES_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import { ML_JOB_AGGREGATION, KIBANA_AGGREGATION, diff --git a/x-pack/plugins/ml/common/util/fields_utils.ts b/x-pack/plugins/ml/common/util/fields_utils.ts index 53deb351df160..517bddd065592 100644 --- a/x-pack/plugins/ml/common/util/fields_utils.ts +++ b/x-pack/plugins/ml/common/util/fields_utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ES_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import { Field, Aggregation, diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index 15e3600d67fe3..ee467fb87f75d 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup } from '@kbn/core/public'; -import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.test.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.test.tsx index d3c848fd5cb1e..cd355f3a8fb36 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.test.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.test.tsx @@ -10,7 +10,7 @@ import { render } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import '@testing-library/jest-dom/extend-expect'; -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { isNumericChartData, diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx index c81d6516184d9..f4c372d379357 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx @@ -15,7 +15,7 @@ import { euiPaletteColorBlind, EuiDataGridColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { isNumericChartData, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index 3ab82daa6b1f3..db181715f6c1d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; import { getNumTopClasses, getNumTopFeatureImportanceValues } from './analytics'; import { Field } from '../../../../common/types/fields'; import { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts index 50f3fa6aeb659..fe1a99519ae66 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import { newJobCapsServiceAnalytics } from '../../services/new_job_capabilities/new_job_capabilities_service_analytics'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts index 881b533efba09..4615dec8f3320 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import { EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx index 9e6d2ee0ad863..3bfdb461a4085 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx @@ -9,7 +9,7 @@ import React, { FC, Fragment, useState, useEffect } from 'react'; import { EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state'; import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/request.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/request.ts index 8fa8d34ad1b6f..ff8cf431103d7 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/request.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/common/request.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { MlJobFieldType } from '../../../../../common/types/field_types'; export interface FieldRequestConfig { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts index 9fd9e4aaff576..e7ad6802c4401 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts @@ -11,7 +11,7 @@ import { get, union, uniq } from 'lodash'; import moment from 'moment-timezone'; -import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import { asyncForEach } from '@kbn/std'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index 01e62a14f300b..9edb81907722e 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -7,7 +7,7 @@ import { BehaviorSubject } from 'rxjs'; import { cloneDeep } from 'lodash'; -import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import type { DataView } from '@kbn/data-views-plugin/public'; import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { UrlConfig } from '../../../../../../common/types/custom_urls'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index 1c8bf04b23931..3112a9cc4455b 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; -import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import { Job, Datafeed, Detector } from '../../../../../../../common/types/anomaly_detection_jobs'; import { newJobCapsService } from '../../../../../services/new_job_capabilities/new_job_capabilities_service'; import { NavigateToPath } from '../../../../../contexts/kibana'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/utils.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/utils.ts index 7babb138b807c..ed0c4639c47c2 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/utils.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/utils.ts @@ -16,7 +16,7 @@ import type { import type { SerializableRecord } from '@kbn/utility-types'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import { layerTypes } from '@kbn/lens-plugin/public'; -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { ML_PAGES, ML_APP_LOCATOR } from '../../../../../common/constants/locator'; import { ML_JOB_AGGREGATION } from '../../../../../common/constants/aggregation_types'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx index b80f946d56d5c..01cf8a666e1d5 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx @@ -17,7 +17,7 @@ import { EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import { useMlKibana, useNavigateToPath } from '../../../../contexts/kibana'; import { useMlContext } from '../../../../contexts/ml'; diff --git a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities.ts b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities.ts index 3d79672bcfed9..2bc51a10ab9d2 100644 --- a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities.ts +++ b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import { Aggregation, Field, NewJobCaps } from '../../../../common/types/fields'; // create two lists, one removing text fields if there are keyword equivalents and vice versa diff --git a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts index 12602c042958d..a30a516e865b3 100644 --- a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts +++ b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { Field, Aggregation, AggId, FieldId } from '../../../../common/types/fields'; import { EVENT_RATE_FIELD_ID } from '../../../../common/types/fields'; diff --git a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts index d3b74dea3669d..5006456ebb5eb 100644 --- a/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts +++ b/x-pack/plugins/ml/public/application/services/new_job_capabilities/new_job_capabilities_service_analytics.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import { DataView } from '@kbn/data-views-plugin/public'; import { Field, NewJobCapsResponse } from '../../../../common/types/fields'; import { processTextAndKeywordFields, NewJobCapabilitiesServiceBase } from './new_job_capabilities'; diff --git a/x-pack/plugins/ml/public/application/util/field_types_utils.test.ts b/x-pack/plugins/ml/public/application/util/field_types_utils.test.ts index 655f78c681c93..a01e15366a96a 100644 --- a/x-pack/plugins/ml/public/application/util/field_types_utils.test.ts +++ b/x-pack/plugins/ml/public/application/util/field_types_utils.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; -import { DataViewField } from '@kbn/data-views-plugin/common'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; import { kbnTypeToMLJobType, diff --git a/x-pack/plugins/ml/public/application/util/field_types_utils.ts b/x-pack/plugins/ml/public/application/util/field_types_utils.ts index e996f03ae3982..5440b87126dd1 100644 --- a/x-pack/plugins/ml/public/application/util/field_types_utils.ts +++ b/x-pack/plugins/ml/public/application/util/field_types_utils.ts @@ -7,8 +7,8 @@ import { i18n } from '@kbn/i18n'; -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; -import { DataViewField } from '@kbn/data-views-plugin/common'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; // convert kibana types to ML Job types diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts index 18cc00c5678ea..4e52a0ddee38d 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts @@ -8,7 +8,7 @@ import { cloneDeep } from 'lodash'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { IScopedClusterClient } from '@kbn/core/server'; -import { ES_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import type { DataViewsService } from '@kbn/data-views-plugin/common'; import type { Field, FieldId, NewJobCaps, RollupFields } from '../../../../common/types/fields'; import { combineFieldsAndAggs } from '../../../../common/util/fields_utils'; diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts index 6d75b8c587eac..c75303be46941 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts @@ -6,7 +6,7 @@ */ import { IScopedClusterClient } from '@kbn/core/server'; -import { ES_FIELD_TYPES } from '@kbn/data-plugin/server'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import { parseInterval } from '../../../common/util/parse_interval'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import { validateJobObject } from './validate_job_object'; diff --git a/x-pack/plugins/transform/common/api_schemas/transforms.ts b/x-pack/plugins/transform/common/api_schemas/transforms.ts index 9d74a7a37fb73..28634e280ee34 100644 --- a/x-pack/plugins/transform/common/api_schemas/transforms.ts +++ b/x-pack/plugins/transform/common/api_schemas/transforms.ts @@ -7,7 +7,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; -import type { ES_FIELD_TYPES } from '@kbn/data-plugin/common'; +import type { ES_FIELD_TYPES } from '@kbn/field-types'; import type { Dictionary } from '../types/common'; import type { PivotAggDict } from '../types/pivot_aggs'; diff --git a/x-pack/plugins/transform/public/app/common/pivot_aggs.ts b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts index 4349b4962556f..a1b8b6fefed0a 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_aggs.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts @@ -7,7 +7,7 @@ import { FC } from 'react'; -import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { AggName } from '../../../common/types/aggregations'; diff --git a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts index b0fa78e8a902f..04f524a3f4163 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { AggName } from '../../../common/types/aggregations'; import { Dictionary } from '../../../common/types/common'; diff --git a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts index 5ebc4eec6bafc..61e6baf5c250e 100644 --- a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts +++ b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts @@ -9,7 +9,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { IHttpFetchError } from '@kbn/core-http-browser'; -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import type { TransformId } from '../../../../common/types/transform'; import type { FieldHistogramsResponseSchema } from '../../../../common/api_schemas/field_histograms'; diff --git a/x-pack/plugins/transform/public/app/hooks/use_api.ts b/x-pack/plugins/transform/public/app/hooks/use_api.ts index 3485820af3a66..de313ecc5603b 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_api.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_api.ts @@ -11,7 +11,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { IHttpFetchError } from '@kbn/core-http-browser'; -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import type { GetTransformsAuditMessagesResponseSchema } from '../../../common/api_schemas/audit_messages'; import type { diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.test.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.test.ts index b38e77745ba99..6c354c1ed953e 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.test.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.test.ts @@ -6,7 +6,7 @@ */ import { getCombinedProperties } from './use_pivot_data'; -import { ES_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; describe('getCombinedProperties', () => { test('extracts missing mappings from docs', () => { diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts index ce94da34fce79..ccd4ae44f1e62 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { getFlattenedObject } from '@kbn/std'; import { difference } from 'lodash'; -import { ES_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; import type { PreviewMappingsProperties } from '../../../common/api_schemas/transforms'; import { isPostTransformsPreviewResponseSchema } from '../../../common/api_schemas/type_guards'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx index 16febe74114b0..be37b91c61157 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { I18nProvider } from '@kbn/i18n-react'; import { FilterAggForm } from './filter_agg_form'; import { CreateTransformWizardContext } from '../../../../wizard/wizard'; -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import type { RuntimeField } from '@kbn/data-views-plugin/common'; import { DataView } from '@kbn/data-views-plugin/public'; import { FilterTermForm } from './filter_term_form'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/constants.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/constants.ts index 20f6828f47beb..7c1265bafd115 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/constants.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/constants.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { FilterAggType } from './types'; export const FILTERS = { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts index 0aa29ed5af3b2..41cce79a67143 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts @@ -6,7 +6,7 @@ */ import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; import { DataView } from '@kbn/data-views-plugin/public'; import { getNestedProperty } from '../../../../../../../common/utils/object_utils'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts index a8a9b5c1e35b0..c8dc63cae1f9a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { EsFieldName } from '../../../../../../../common/types/fields'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_latest_function_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_latest_function_config.ts index fe4d332f4a91b..4e395cf89335e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_latest_function_config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_latest_function_config.ts @@ -8,7 +8,7 @@ import { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { AggConfigs, FieldParamType } from '@kbn/data-plugin/common'; +import type { AggConfigs, FieldParamType } from '@kbn/data-plugin/common'; import { LatestFunctionConfigUI } from '../../../../../../../common/types/transform'; import { StepDefineFormProps } from '../step_define_form'; import { StepDefineExposedState } from '../common'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts index 7be29aa688824..962bc45f3d212 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts @@ -8,7 +8,7 @@ import { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { AggName } from '../../../../../../../common/types/aggregations'; import { dictionaryToArray, isDefined } from '../../../../../../../common/types/common'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index 7357a97ef5e1e..07bd0cff951af 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -24,7 +24,7 @@ import { EuiText, } from '@elastic/eui'; -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { isHttpFetchError } from '@kbn/core-http-browser'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx index 41e628d495d49..b9c5c52974d1b 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx @@ -19,7 +19,7 @@ import { import { i18n } from '@kbn/i18n'; -import { KBN_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { isEsIngestPipelines } from '../../../../../../common/api_schemas/type_guards'; import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; import { UseEditTransformFlyoutReturnType } from './use_edit_transform_flyout'; From a6c6efbee4c31595b59f8d73e0c49f4783eee6a5 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 20 Dec 2022 13:01:47 +0200 Subject: [PATCH 45/55] [Discover] Enables the inspector for the text based languages (#147833) ## Summary Enables the inspector on the text based languages (it already works in Lens) image --- .../public/application/main/utils/fetch_all.ts | 11 +++++++++-- .../public/application/main/utils/fetch_sql.ts | 6 +++++- .../public/embeddable/saved_search_embeddable.tsx | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index d782442db3953..3057a7992f693 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -52,7 +52,14 @@ export function fetchAll( reset = false, fetchDeps: FetchDeps ): Promise { - const { initialFetchStatus, appStateContainer, services, useNewFieldsApi, data } = fetchDeps; + const { + initialFetchStatus, + appStateContainer, + services, + useNewFieldsApi, + data, + inspectorAdapters, + } = fetchDeps; try { const dataView = searchSource.getField('index')!; @@ -81,7 +88,7 @@ export function fetchAll( // Start fetching all required requests const documents = useSql && query - ? fetchSql(query, services.dataViews, data, services.expressions) + ? fetchSql(query, services.dataViews, data, services.expressions, inspectorAdapters) : fetchDocuments(searchSource.createCopy(), fetchDeps); // Handle results of the individual queries and forward the results to the corresponding dataSubjects diff --git a/src/plugins/discover/public/application/main/utils/fetch_sql.ts b/src/plugins/discover/public/application/main/utils/fetch_sql.ts index 057ddc5886da5..07a8177d84365 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_sql.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_sql.ts @@ -8,6 +8,7 @@ import { pluck } from 'rxjs/operators'; import { lastValueFrom } from 'rxjs'; import { Query, AggregateQuery, Filter } from '@kbn/es-query'; +import type { Adapters } from '@kbn/inspector-plugin/common'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; import type { Datatable } from '@kbn/expressions-plugin/public'; @@ -27,6 +28,7 @@ export function fetchSql( dataViewsService: DataViewsContract, data: DataPublicPluginStart, expressions: ExpressionsStart, + inspectorAdapters: Adapters, filters?: Filter[], inputQuery?: Query ) { @@ -40,7 +42,9 @@ export function fetchSql( }) .then((ast) => { if (ast) { - const execution = expressions.run(ast, null); + const execution = expressions.run(ast, null, { + inspectorAdapters, + }); let finalData: DataTableRecord[] = []; let error: string | undefined; execution.pipe(pluck('result')).subscribe((resp) => { diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index 3d47a97344d6a..a98fb03539127 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -230,6 +230,7 @@ export class SavedSearchEmbeddable this.services.dataViews, this.services.data, this.services.expressions, + this.services.inspector, this.input.filters, this.input.query ); From 374478d5d3959874af79bc027b1392835a8724d7 Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Tue, 20 Dec 2022 12:13:10 +0100 Subject: [PATCH 46/55] Add image size for no data in Alerts Table to avoid layout shift (#147706) Fixes #138410 ## Summary I've added image size to avoid a layout shift in the empty state component. Also added a storybook for that component. --- .../alerts_table/empty_state.stories.tsx | 22 +++++++++++++++++++ .../sections/alerts_table/empty_state.tsx | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/empty_state.stories.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/empty_state.stories.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/empty_state.stories.tsx new file mode 100644 index 0000000000000..87227cccc242d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/empty_state.stories.tsx @@ -0,0 +1,22 @@ +/* + * 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 { EmptyState as Component } from './empty_state'; + +export default { + component: Component, + title: 'app/AlertTable', + argTypes: { + height: { type: 'select', options: ['short', 'tall'] }, + }, +}; + +export const EmptyState = { + args: { + height: 'tall', + }, +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/empty_state.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/empty_state.tsx index a022fa08f1e07..1a8ee2c3e5966 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/empty_state.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/empty_state.tsx @@ -45,7 +45,7 @@ export const EmptyState: React.FC<{ height?: keyof typeof heights }> = ({ height - + From aaac0110bdae05a780addb52af9507035e5b98b6 Mon Sep 17 00:00:00 2001 From: Juan Pablo Djeredjian Date: Tue, 20 Dec 2022 12:28:01 +0100 Subject: [PATCH 47/55] [Security Solution] Add skipped rules to Bulk Edit rules API response (#147345) **Addresses:** https://github.com/elastic/kibana/issues/145093 **Related to:** https://github.com/elastic/kibana/issues/139802 ## Summary - Extends Bulk Edit API to return a new `skipped` property for rules whose updating was skipped. See [#145093](https://github.com/elastic/kibana/issues/145093) for details on when a rule is skipped. - In `x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts`, refactored the methods `bulkEdit` and `bulkEditOcc` to smaller methods, following an immutable approach. - Updated all related tests and expanded coverage. (unit, integration and e2e) - Update success toast message so that the user is informed if rules were skipped. https://user-images.githubusercontent.com/5354282/199806913-eb70e7a6-0435-486a-96f1-dd0e8abaffe2.mp4 ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - https://github.com/elastic/security-docs/issues/2684 - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) Co-authored-by: Xavier Mouligneau --- x-pack/plugins/alerting/common/bulk_edit.ts | 16 + x-pack/plugins/alerting/common/index.ts | 1 + .../server/routes/bulk_edit_rules.test.ts | 3 +- .../common/apply_bulk_edit_operation.test.ts | 316 +++++---- .../common/apply_bulk_edit_operation.ts | 9 +- .../retry_if_bulk_edit_conflicts.test.ts | 39 +- .../common/retry_if_bulk_edit_conflicts.ts | 19 +- .../server/rules_client/methods/bulk_edit.ts | 665 ++++++++++++------ .../rules_client/tests/bulk_edit.test.ts | 586 ++++++++++++++- .../api/rules/bulk_actions/request_schema.ts | 4 +- .../api/rules/bulk_actions/response_schema.ts | 53 ++ .../e2e/detection_rules/bulk_edit_rules.cy.ts | 49 +- .../bulk_edit_rules_actions.cy.ts | 6 +- .../bulk_edit_rules_data_view.cy.ts | 115 ++- .../cypress/tasks/rules_bulk_edit.ts | 37 +- .../rule_management/api/api.ts | 2 + .../logic/bulk_actions/translations.ts | 13 +- .../components/rules_table/helpers.ts | 1 + .../detection_engine/rules/translations.ts | 24 +- .../routes/__mocks__/test_adapters.ts | 5 +- .../api/rules/bulk_actions/route.test.ts | 138 +++- .../api/rules/bulk_actions/route.ts | 84 ++- .../logic/bulk_actions/bulk_edit_rules.ts | 1 - .../bulk_actions/rule_params_modifier.test.ts | 111 ++- .../bulk_actions/rule_params_modifier.ts | 111 ++- .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../group3/tests/alerting/bulk_edit.ts | 7 +- .../group10/perform_bulk_action.ts | 489 ++++++++++--- .../group10/perform_bulk_action_dry_run.ts | 70 +- 31 files changed, 2313 insertions(+), 667 deletions(-) create mode 100644 x-pack/plugins/alerting/common/bulk_edit.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/response_schema.ts diff --git a/x-pack/plugins/alerting/common/bulk_edit.ts b/x-pack/plugins/alerting/common/bulk_edit.ts new file mode 100644 index 0000000000000..73773032f5280 --- /dev/null +++ b/x-pack/plugins/alerting/common/bulk_edit.ts @@ -0,0 +1,16 @@ +/* + * 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 { Rule } from './rule'; + +export type BulkEditSkipReason = 'RULE_NOT_MODIFIED'; + +export interface BulkActionSkipResult { + id: Rule['id']; + name?: Rule['name']; + skip_reason: BulkEditSkipReason; +} diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerting/common/index.ts index eeb3db0be0066..795a05dcb802c 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerting/common/index.ts @@ -17,6 +17,7 @@ export * from './rule_navigation'; export * from './alert_instance'; export * from './alert_summary'; export * from './builtin_action_groups'; +export * from './bulk_edit'; export * from './disabled_action_groups'; export * from './rule_notify_when_type'; export * from './parse_duration'; diff --git a/x-pack/plugins/alerting/server/routes/bulk_edit_rules.test.ts b/x-pack/plugins/alerting/server/routes/bulk_edit_rules.test.ts index b70e4734ab4ff..0ff709c252f56 100644 --- a/x-pack/plugins/alerting/server/routes/bulk_edit_rules.test.ts +++ b/x-pack/plugins/alerting/server/routes/bulk_edit_rules.test.ts @@ -71,7 +71,7 @@ describe('bulkEditInternalRulesRoute', () => { }, ], }; - const bulkEditResult = { rules: mockedAlerts, errors: [], total: 1 }; + const bulkEditResult = { rules: mockedAlerts, errors: [], total: 1, skipped: [] }; it('bulk edits rules with tags action', async () => { const licenseState = licenseStateMock.create(); @@ -97,6 +97,7 @@ describe('bulkEditInternalRulesRoute', () => { body: { total: 1, errors: [], + skipped: [], rules: [ expect.objectContaining({ id: '1', diff --git a/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.test.ts b/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.test.ts index 03d688b6bcb3f..49ed183ceb39d 100644 --- a/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.test.ts @@ -14,96 +14,166 @@ describe('applyBulkEditOperation', () => { const ruleMock: Partial = { tags: ['tag-1', 'tag-2'], }; - expect( - applyBulkEditOperation( - { - field: 'tags', - value: ['add-tag'], - operation: 'add', - }, - ruleMock - ) - ).toHaveProperty('tags', ['tag-1', 'tag-2', 'add-tag']); + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'tags', + value: ['add-tag'], + operation: 'add', + }, + ruleMock + ); + + expect(modifiedAttributes).toHaveProperty('tags', ['tag-1', 'tag-2', 'add-tag']); + expect(isAttributeModified).toBe(true); }); test('should add multiple tags', () => { const ruleMock: Partial = { tags: ['tag-1', 'tag-2'], }; - expect( - applyBulkEditOperation( - { - field: 'tags', - value: ['add-tag-1', 'add-tag-2'], - operation: 'add', - }, - ruleMock - ) - ).toHaveProperty('tags', ['tag-1', 'tag-2', 'add-tag-1', 'add-tag-2']); + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'tags', + value: ['add-tag-1', 'add-tag-2'], + operation: 'add', + }, + ruleMock + ); + expect(modifiedAttributes).toHaveProperty('tags', [ + 'tag-1', + 'tag-2', + 'add-tag-1', + 'add-tag-2', + ]); + expect(isAttributeModified).toBe(true); }); test('should not have duplicated tags when added existed ones', () => { const ruleMock: Partial = { tags: ['tag-1', 'tag-2'], }; - expect( - applyBulkEditOperation( - { - field: 'tags', - value: ['tag-1', 'tag-3'], - operation: 'add', - }, - ruleMock - ) - ).toHaveProperty('tags', ['tag-1', 'tag-2', 'tag-3']); + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'tags', + value: ['tag-1', 'tag-3'], + operation: 'add', + }, + ruleMock + ); + expect(modifiedAttributes).toHaveProperty('tags', ['tag-1', 'tag-2', 'tag-3']); + expect(isAttributeModified).toBe(true); }); test('should delete tag', () => { const ruleMock: Partial = { tags: ['tag-1', 'tag-2'], }; - expect( - applyBulkEditOperation( - { - field: 'tags', - value: ['tag-1'], - operation: 'delete', - }, - ruleMock - ) - ).toHaveProperty('tags', ['tag-2']); + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'tags', + value: ['tag-1'], + operation: 'delete', + }, + ruleMock + ); + expect(modifiedAttributes).toHaveProperty('tags', ['tag-2']); + expect(isAttributeModified).toBe(true); }); test('should delete multiple tags', () => { const ruleMock: Partial = { tags: ['tag-1', 'tag-2'], }; - expect( - applyBulkEditOperation( - { - field: 'tags', - value: ['tag-1', 'tag-2'], - operation: 'delete', - }, - ruleMock - ) - ).toHaveProperty('tags', []); + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'tags', + value: ['tag-1', 'tag-2'], + operation: 'delete', + }, + ruleMock + ); + expect(modifiedAttributes).toHaveProperty('tags', []); + expect(isAttributeModified).toBe(true); }); test('should rewrite tags', () => { const ruleMock: Partial = { tags: ['tag-1', 'tag-2'], }; - expect( - applyBulkEditOperation( - { - field: 'tags', - value: ['rewrite-tag'], - operation: 'set', - }, - ruleMock - ) - ).toHaveProperty('tags', ['rewrite-tag']); + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'tags', + value: ['rewrite-tag'], + operation: 'set', + }, + ruleMock + ); + expect(modifiedAttributes).toHaveProperty('tags', ['rewrite-tag']); + expect(isAttributeModified).toBe(true); + }); + + test('should return isAttributeModified=false when only adding already existing tags', () => { + const ruleMock: Partial = { + tags: ['tag-1', 'tag-2'], + }; + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'tags', + value: ['tag-1', 'tag-2'], + operation: 'add', + }, + ruleMock + ); + expect(modifiedAttributes).toHaveProperty('tags', ['tag-1', 'tag-2']); + expect(isAttributeModified).toBe(false); + }); + + test('should return isAttributeModified=false when adding no tags', () => { + const ruleMock: Partial = { + tags: ['tag-1', 'tag-2'], + }; + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'tags', + value: [], + operation: 'add', + }, + ruleMock + ); + expect(modifiedAttributes).toHaveProperty('tags', ['tag-1', 'tag-2']); + expect(isAttributeModified).toBe(false); + }); + + test('should return isAttributeModified=false when deleting no tags', () => { + const ruleMock: Partial = { + tags: ['tag-1', 'tag-2'], + }; + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'tags', + value: [], + operation: 'delete', + }, + ruleMock + ); + expect(modifiedAttributes).toHaveProperty('tags', ['tag-1', 'tag-2']); + expect(isAttributeModified).toBe(false); + }); + + test('should return isAttributeModified=false when deleting non-existing tags', () => { + const ruleMock: Partial = { + tags: ['tag-1', 'tag-2'], + }; + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'tags', + value: ['tag-3'], + operation: 'delete', + }, + ruleMock + ); + expect(modifiedAttributes).toHaveProperty('tags', ['tag-1', 'tag-2']); + expect(isAttributeModified).toBe(false); }); }); @@ -112,60 +182,60 @@ describe('applyBulkEditOperation', () => { const ruleMock = { actions: [{ id: 'mock-action-id', group: 'default', params: {} }], }; - expect( - applyBulkEditOperation( - { - field: 'actions', - value: [ - { id: 'mock-add-action-id-1', group: 'default', params: {} }, - { id: 'mock-add-action-id-2', group: 'default', params: {} }, - ], - operation: 'add', - }, - ruleMock - ) - ).toHaveProperty('actions', [ + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'actions', + value: [ + { id: 'mock-add-action-id-1', group: 'default', params: {} }, + { id: 'mock-add-action-id-2', group: 'default', params: {} }, + ], + operation: 'add', + }, + ruleMock + ); + expect(modifiedAttributes).toHaveProperty('actions', [ { id: 'mock-action-id', group: 'default', params: {} }, { id: 'mock-add-action-id-1', group: 'default', params: {} }, { id: 'mock-add-action-id-2', group: 'default', params: {} }, ]); + expect(isAttributeModified).toBe(true); }); test('should add action with different params and same id', () => { const ruleMock = { actions: [{ id: 'mock-action-id', group: 'default', params: { test: 1 } }], }; - expect( - applyBulkEditOperation( - { - field: 'actions', - value: [{ id: 'mock-action-id', group: 'default', params: { test: 2 } }], - operation: 'add', - }, - ruleMock - ) - ).toHaveProperty('actions', [ + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'actions', + value: [{ id: 'mock-action-id', group: 'default', params: { test: 2 } }], + operation: 'add', + }, + ruleMock + ); + expect(modifiedAttributes).toHaveProperty('actions', [ { id: 'mock-action-id', group: 'default', params: { test: 1 } }, { id: 'mock-action-id', group: 'default', params: { test: 2 } }, ]); + expect(isAttributeModified).toBe(true); }); test('should rewrite actions', () => { const ruleMock = { actions: [{ id: 'mock-action-id', group: 'default', params: {} }], }; - expect( - applyBulkEditOperation( - { - field: 'actions', - value: [{ id: 'mock-rewrite-action-id-1', group: 'default', params: {} }], - operation: 'set', - }, - ruleMock - ) - ).toHaveProperty('actions', [ + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'actions', + value: [{ id: 'mock-rewrite-action-id-1', group: 'default', params: {} }], + operation: 'set', + }, + ruleMock + ); + expect(modifiedAttributes).toHaveProperty('actions', [ { id: 'mock-rewrite-action-id-1', group: 'default', params: {} }, ]); + expect(isAttributeModified).toBe(true); }); }); @@ -174,16 +244,16 @@ describe('applyBulkEditOperation', () => { const ruleMock = { actions: [{ id: 'mock-action-id', group: 'default', params: {} }], }; - expect( - applyBulkEditOperation( - { - field: 'throttle', - value: '1d', - operation: 'set', - }, - ruleMock - ) - ).toHaveProperty('throttle', '1d'); + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'throttle', + value: '1d', + operation: 'set', + }, + ruleMock + ); + expect(modifiedAttributes).toHaveProperty('throttle', '1d'); + expect(isAttributeModified).toBe(true); }); }); @@ -192,16 +262,16 @@ describe('applyBulkEditOperation', () => { const ruleMock = { actions: [{ id: 'mock-action-id', group: 'default', params: {} }], }; - expect( - applyBulkEditOperation( - { - field: 'notifyWhen', - value: 'onThrottleInterval', - operation: 'set', - }, - ruleMock - ) - ).toHaveProperty('notifyWhen', 'onThrottleInterval'); + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'notifyWhen', + value: 'onThrottleInterval', + operation: 'set', + }, + ruleMock + ); + expect(modifiedAttributes).toHaveProperty('notifyWhen', 'onThrottleInterval'); + expect(isAttributeModified).toBe(true); }); }); @@ -210,16 +280,16 @@ describe('applyBulkEditOperation', () => { const ruleMock = { actions: [{ id: 'mock-action-id', group: 'default', params: {} }], }; - expect( - applyBulkEditOperation( - { - field: 'schedule', - value: { interval: '1d' }, - operation: 'set', - }, - ruleMock - ) - ).toHaveProperty('schedule', { interval: '1d' }); + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + { + field: 'schedule', + value: { interval: '1d' }, + operation: 'set', + }, + ruleMock + ); + expect(modifiedAttributes).toHaveProperty('schedule', { interval: '1d' }); + expect(isAttributeModified).toBe(true); }); }); }); diff --git a/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.ts b/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.ts index e40d8e6c8c854..b5ca53642496e 100644 --- a/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/apply_bulk_edit_operation.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { set, get } from 'lodash'; +import { set, get, isEqual } from 'lodash'; import type { BulkEditOperation, BulkEditFields } from '../types'; // defining an union type that will passed directly to generic function as a workaround for the issue similar to @@ -27,6 +27,8 @@ export const applyBulkEditOperation = (operation: BulkEditOper return arr.filter((item) => !itemsSet.has(item)); }; + const originalFieldValue = get(rule, operation.field); + switch (operation.operation) { case 'set': set(rule, operation.field, operation.value); @@ -49,5 +51,8 @@ export const applyBulkEditOperation = (operation: BulkEditOper break; } - return rule; + return { + modifiedAttributes: rule, + isAttributeModified: !isEqual(originalFieldValue, get(rule, operation.field)), + }; }; diff --git a/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.test.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.test.ts index 5053b367b938e..c5570a7422d02 100644 --- a/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.test.ts @@ -9,6 +9,7 @@ import { KueryNode } from '@kbn/es-query'; import { retryIfBulkEditConflicts } from './retry_if_bulk_edit_conflicts'; import { RETRY_IF_CONFLICTS_ATTEMPTS } from './wait_before_next_retry'; import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { BulkEditSkipReason } from '../../../common/bulk_edit'; const mockFilter: KueryNode = { type: 'function', @@ -28,6 +29,11 @@ const mockSuccessfulResult = { { id: '2', type: 'alert', attributes: { name: 'Test rule 2' }, references: [] }, ], errors: [], + skipped: [ + { id: 'skip-1', name: 'skip-1', skip_reason: 'RULE_NOT_MODIFIED' as BulkEditSkipReason }, + { id: 'skip-2', name: 'skip-2', skip_reason: 'RULE_NOT_MODIFIED' as BulkEditSkipReason }, + { id: 'skip-5', name: 'skip-5', skip_reason: 'RULE_NOT_MODIFIED' as BulkEditSkipReason }, + ], }; async function OperationSuccessful() { @@ -78,6 +84,11 @@ describe('retryIfBulkEditConflicts', () => { expect(result).toEqual({ apiKeysToInvalidate: [], errors: [], + skipped: [ + { id: 'skip-1', name: 'skip-1', skip_reason: 'RULE_NOT_MODIFIED' }, + { id: 'skip-2', name: 'skip-2', skip_reason: 'RULE_NOT_MODIFIED' }, + { id: 'skip-5', name: 'skip-5', skip_reason: 'RULE_NOT_MODIFIED' }, + ], results: [ { attributes: {}, @@ -130,7 +141,7 @@ describe('retryIfBulkEditConflicts', () => { expect(mockLogger.warn).toBeCalledWith(`${mockOperationName} conflicts, exceeded retries`); }); - for (let i = 1; i <= RETRY_IF_CONFLICTS_ATTEMPTS; i++) { + for (let i = 1; i <= RETRY_IF_CONFLICTS_ATTEMPTS + 1; i++) { test(`should work when operation conflicts ${i} times`, async () => { const result = await retryIfBulkEditConflicts( mockLogger, @@ -141,4 +152,30 @@ describe('retryIfBulkEditConflicts', () => { expect(result).toBe(result); }); } + + test('should not return duplicated skip results when the underlying bulkEditOperation is retried multiple times and returns the same skip results on every attempt', async () => { + const result = await retryIfBulkEditConflicts( + mockLogger, + mockOperationName, + getOperationConflictsTimes(3), + mockFilter, + undefined, + ['apikey1', 'apikey2'], + [], + [], + [ + { id: 'skip-1', name: 'skip-1', skip_reason: 'RULE_NOT_MODIFIED' }, + { id: 'skip-2', name: 'skip-2', skip_reason: 'RULE_NOT_MODIFIED' }, + { id: 'skip-2', name: 'skip-2', skip_reason: 'RULE_NOT_MODIFIED' }, + { id: 'skip-3', name: 'skip-3', skip_reason: 'RULE_NOT_MODIFIED' }, + ] + ); + + expect(result.skipped).toEqual([ + { id: 'skip-1', name: 'skip-1', skip_reason: 'RULE_NOT_MODIFIED' }, + { id: 'skip-2', name: 'skip-2', skip_reason: 'RULE_NOT_MODIFIED' }, + { id: 'skip-3', name: 'skip-3', skip_reason: 'RULE_NOT_MODIFIED' }, + { id: 'skip-5', name: 'skip-5', skip_reason: 'RULE_NOT_MODIFIED' }, + ]); + }); }); diff --git a/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.ts index 984ce17d149e0..8d03d01df5af4 100644 --- a/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_edit_conflicts.ts @@ -9,6 +9,7 @@ import pMap from 'p-map'; import { chunk } from 'lodash'; import { KueryNode } from '@kbn/es-query'; import { Logger, SavedObjectsBulkUpdateObject, SavedObjectsUpdateResponse } from '@kbn/core/server'; +import { BulkActionSkipResult } from '../../../common/bulk_edit'; import { convertRuleIdsToKueryNode } from '../../lib'; import { BulkOperationError } from '../types'; import { RawRule } from '../../types'; @@ -22,12 +23,14 @@ type BulkEditOperation = (filter: KueryNode | null) => Promise<{ rules: Array>; resultSavedObjects: Array>; errors: BulkOperationError[]; + skipped: BulkActionSkipResult[]; }>; interface ReturnRetry { apiKeysToInvalidate: string[]; results: Array>; errors: BulkOperationError[]; + skipped: BulkActionSkipResult[]; } /** @@ -52,7 +55,8 @@ export const retryIfBulkEditConflicts = async ( retries: number = RETRY_IF_CONFLICTS_ATTEMPTS, accApiKeysToInvalidate: string[] = [], accResults: Array> = [], - accErrors: BulkOperationError[] = [] + accErrors: BulkOperationError[] = [], + accSkipped: BulkActionSkipResult[] = [] ): Promise => { // run the operation, return if no errors or throw if not a conflict error try { @@ -61,6 +65,7 @@ export const retryIfBulkEditConflicts = async ( resultSavedObjects, errors: localErrors, rules: localRules, + skipped: localSkipped, } = await bulkEditOperation(filter); const conflictErrorMap = resultSavedObjects.reduce>( @@ -76,12 +81,17 @@ export const retryIfBulkEditConflicts = async ( const results = [...accResults, ...resultSavedObjects.filter((res) => res.error === undefined)]; const apiKeysToInvalidate = [...accApiKeysToInvalidate, ...localApiKeysToInvalidate]; const errors = [...accErrors, ...localErrors]; + // Create array of unique skipped rules by id + const skipped = [ + ...new Map([...accSkipped, ...localSkipped].map((item) => [item.id, item])).values(), + ]; if (conflictErrorMap.size === 0) { return { apiKeysToInvalidate, results, errors, + skipped, }; } @@ -102,6 +112,7 @@ export const retryIfBulkEditConflicts = async ( apiKeysToInvalidate, results, errors: [...errors, ...conflictErrors], + skipped, }; } @@ -126,7 +137,8 @@ export const retryIfBulkEditConflicts = async ( retries - 1, apiKeysToInvalidate, results, - errors + errors, + skipped ), { concurrency: 1, @@ -138,9 +150,10 @@ export const retryIfBulkEditConflicts = async ( results: [...acc.results, ...item.results], apiKeysToInvalidate: [...acc.apiKeysToInvalidate, ...item.apiKeysToInvalidate], errors: [...acc.errors, ...item.errors], + skipped: [...acc.skipped, ...item.skipped], }; }, - { results: [], apiKeysToInvalidate: [], errors: [] } + { results: [], apiKeysToInvalidate: [], errors: [], skipped: [] } ); } catch (err) { throw err; diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts index cae2ebc2406bb..dc97258fa994d 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts @@ -10,8 +10,22 @@ import Boom from '@hapi/boom'; import { cloneDeep } from 'lodash'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { KueryNode, nodeBuilder } from '@kbn/es-query'; -import { SavedObjectsBulkUpdateObject, SavedObjectsUpdateResponse } from '@kbn/core/server'; -import { RawRule, SanitizedRule, RuleTypeParams, Rule, RuleSnoozeSchedule } from '../../types'; +import { + SavedObjectsBulkUpdateObject, + SavedObjectsFindResult, + SavedObjectsUpdateResponse, +} from '@kbn/core/server'; +import { BulkActionSkipResult } from '../../../common/bulk_edit'; +import { + RawRule, + SanitizedRule, + RuleTypeParams, + Rule, + RuleSnoozeSchedule, + RuleWithLegacyId, + RuleTypeRegistry, + RawRuleAction, +} from '../../types'; import { validateRuleTypeParams, getRuleNotifyWhenType, @@ -46,6 +60,7 @@ import { BulkOperationError, RuleBulkOperationAggregation, RulesClientContext, + CreateAPIKeyResult, } from '../types'; export type BulkEditFields = keyof Pick< @@ -95,7 +110,20 @@ export type BulkEditOperation = value?: undefined; }; -type RuleParamsModifier = (params: Params) => Promise; +type ApiKeysMap = Map; + +type ApiKeyAttributes = Pick; + +type RuleType = ReturnType; + +export interface RuleParamsModifierResult { + modifiedParams: Params; + isParamsUpdateSkipped: boolean; +} + +export type RuleParamsModifier = ( + params: Params +) => Promise>; export interface BulkEditOptionsFilter { filter?: string | KueryNode; @@ -118,6 +146,7 @@ export async function bulkEdit( options: BulkEditOptions ): Promise<{ rules: Array>; + skipped: BulkActionSkipResult[]; errors: BulkOperationError[]; total: number; }> { @@ -210,7 +239,7 @@ export async function bulkEdit( { concurrency: RULE_TYPE_CHECKS_CONCURRENCY } ); - const { apiKeysToInvalidate, results, errors } = await retryIfBulkEditConflicts( + const { apiKeysToInvalidate, results, errors, skipped } = await retryIfBulkEditConflicts( context.logger, `rulesClient.update('operations=${JSON.stringify(options.operations)}, paramsModifier=${ options.paramsModifier ? '[Function]' : undefined @@ -224,11 +253,13 @@ export async function bulkEdit( qNodeFilterWithAuth ); - await bulkMarkApiKeysForInvalidation( - { apiKeys: apiKeysToInvalidate }, - context.logger, - context.unsecuredSavedObjectsClient - ); + if (apiKeysToInvalidate.length > 0) { + await bulkMarkApiKeysForInvalidation( + { apiKeys: apiKeysToInvalidate }, + context.logger, + context.unsecuredSavedObjectsClient + ); + } const updatedRules = results.map(({ id, attributes, references }) => { return getAlertFromRaw( @@ -241,37 +272,9 @@ export async function bulkEdit( ); }); - // update schedules only if schedule operation is present - const scheduleOperation = options.operations.find( - ( - operation - ): operation is Extract }> => - operation.field === 'schedule' - ); - - if (scheduleOperation?.value) { - const taskIds = updatedRules.reduce((acc, rule) => { - if (rule.scheduledTaskId) { - acc.push(rule.scheduledTaskId); - } - return acc; - }, []); - - try { - await context.taskManager.bulkUpdateSchedules(taskIds, scheduleOperation.value); - context.logger.debug( - `Successfully updated schedules for underlying tasks: ${taskIds.join(', ')}` - ); - } catch (error) { - context.logger.error( - `Failure to update schedules for underlying tasks: ${taskIds.join( - ', ' - )}. TaskManager bulkUpdateSchedules failed with Error: ${error.message}` - ); - } - } + await bulkUpdateSchedules(context, options.operations, updatedRules); - return { rules: updatedRules, errors, total }; + return { rules: updatedRules, skipped, errors, total }; } async function bulkEditOcc( @@ -290,6 +293,7 @@ async function bulkEditOcc( rules: Array>; resultSavedObjects: Array>; errors: BulkOperationError[]; + skipped: BulkActionSkipResult[]; }> { const rulesFinder = await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( @@ -302,210 +306,420 @@ async function bulkEditOcc( ); const rules: Array> = []; + const skipped: BulkActionSkipResult[] = []; const errors: BulkOperationError[] = []; - const apiKeysToInvalidate: string[] = []; - const apiKeysMap = new Map(); + const apiKeysMap: ApiKeysMap = new Map(); const username = await context.getUserName(); for await (const response of rulesFinder.find()) { await pMap( response.saved_objects, - async (rule) => { - try { - if (rule.attributes.apiKey) { - apiKeysMap.set(rule.id, { oldApiKey: rule.attributes.apiKey }); - } + async (rule: SavedObjectsFindResult) => + updateRuleAttributesAndParamsInMemory({ + context, + rule, + operations, + paramsModifier, + apiKeysMap, + rules, + skipped, + errors, + username, + }), + { concurrency: API_KEY_GENERATE_CONCURRENCY } + ); + } + await rulesFinder.close(); - const ruleType = context.ruleTypeRegistry.get(rule.attributes.alertTypeId); + const { result, apiKeysToInvalidate } = + rules.length > 0 + ? await saveBulkUpdatedRules(context, rules, apiKeysMap) + : { + result: { saved_objects: [] }, + apiKeysToInvalidate: [], + }; + + return { + apiKeysToInvalidate, + resultSavedObjects: result.saved_objects, + errors, + rules, + skipped, + }; +} - let attributes = cloneDeep(rule.attributes); - let ruleActions = { - actions: injectReferencesIntoActions( - rule.id, - rule.attributes.actions, - rule.references || [] - ), - }; +async function bulkUpdateSchedules( + context: RulesClientContext, + operations: BulkEditOperation[], + updatedRules: Array +): Promise { + const scheduleOperation = operations.find( + ( + operation + ): operation is Extract }> => + operation.field === 'schedule' + ); - for (const operation of operations) { - const { field } = operation; - if (field === 'snoozeSchedule' || field === 'apiKey') { - if (rule.attributes.actions.length) { - try { - await context.actionsAuthorization.ensureAuthorized('execute'); - } catch (error) { - throw Error(`Rule not authorized for bulk ${field} update - ${error.message}`); - } - } - } - } + if (!scheduleOperation?.value) { + return; + } + const taskIds = updatedRules.reduce((acc, rule) => { + if (rule.scheduledTaskId) { + acc.push(rule.scheduledTaskId); + } + return acc; + }, []); - let hasUpdateApiKeyOperation = false; - - for (const operation of operations) { - switch (operation.field) { - case 'actions': - await validateActions(context, ruleType, { - ...attributes, - actions: operation.value, - }); - ruleActions = applyBulkEditOperation(operation, ruleActions); - break; - case 'snoozeSchedule': - // Silently skip adding snooze or snooze schedules on security - // rules until we implement snoozing of their rules - if (attributes.consumer === AlertConsumers.SIEM) { - break; - } - if (operation.operation === 'set') { - const snoozeAttributes = getBulkSnoozeAttributes(attributes, operation.value); - try { - verifySnoozeScheduleLimit(snoozeAttributes); - } catch (error) { - throw Error(`Error updating rule: could not add snooze - ${error.message}`); - } - attributes = { - ...attributes, - ...snoozeAttributes, - }; - } - if (operation.operation === 'delete') { - const idsToDelete = operation.value && [...operation.value]; - if (idsToDelete?.length === 0) { - attributes.snoozeSchedule?.forEach((schedule) => { - if (schedule.id) { - idsToDelete.push(schedule.id); - } - }); - } - attributes = { - ...attributes, - ...getBulkUnsnoozeAttributes(attributes, idsToDelete), - }; - } - break; - case 'apiKey': { - hasUpdateApiKeyOperation = true; - break; - } - default: - attributes = applyBulkEditOperation(operation, attributes); - } - } + try { + await context.taskManager.bulkUpdateSchedules(taskIds, scheduleOperation.value); + context.logger.debug( + `Successfully updated schedules for underlying tasks: ${taskIds.join(', ')}` + ); + } catch (error) { + context.logger.error( + `Failure to update schedules for underlying tasks: ${taskIds.join( + ', ' + )}. TaskManager bulkUpdateSchedules failed with Error: ${error.message}` + ); + } +} - // validate schedule interval - if (attributes.schedule.interval) { - const isIntervalInvalid = - parseDuration(attributes.schedule.interval as string) < - context.minimumScheduleIntervalInMs; - if (isIntervalInvalid && context.minimumScheduleInterval.enforce) { - throw Error( - `Error updating rule: the interval is less than the allowed minimum interval of ${context.minimumScheduleInterval.value}` - ); - } else if (isIntervalInvalid && !context.minimumScheduleInterval.enforce) { - context.logger.warn( - `Rule schedule interval (${attributes.schedule.interval}) for "${ruleType.id}" rule type with ID "${attributes.id}" is less than the minimum value (${context.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent such changes.` - ); - } - } +async function updateRuleAttributesAndParamsInMemory({ + context, + rule, + operations, + paramsModifier, + apiKeysMap, + rules, + skipped, + errors, + username, +}: { + context: RulesClientContext; + rule: SavedObjectsFindResult; + operations: BulkEditOptions['operations']; + paramsModifier: BulkEditOptions['paramsModifier']; + apiKeysMap: ApiKeysMap; + rules: Array>; + skipped: BulkActionSkipResult[]; + errors: BulkOperationError[]; + username: string | null; +}): Promise { + try { + if (rule.attributes.apiKey) { + apiKeysMap.set(rule.id, { oldApiKey: rule.attributes.apiKey }); + } - const ruleParams = paramsModifier - ? await paramsModifier(attributes.params as Params) - : attributes.params; - - // validate rule params - const validatedAlertTypeParams = validateRuleTypeParams( - ruleParams, - ruleType.validate?.params - ); - const validatedMutatedAlertTypeParams = validateMutatedRuleTypeParams( - validatedAlertTypeParams, - rule.attributes.params, - ruleType.validate?.params - ); - - const { - actions: rawAlertActions, - references, - params: updatedParams, - } = await extractReferences( - context, - ruleType, - ruleActions.actions, - validatedMutatedAlertTypeParams - ); - - const shouldUpdateApiKey = attributes.enabled || hasUpdateApiKeyOperation; - - // create API key - let createdAPIKey = null; - try { - createdAPIKey = shouldUpdateApiKey - ? await context.createAPIKey(generateAPIKeyName(ruleType.id, attributes.name)) - : null; - } catch (error) { - throw Error(`Error updating rule: could not create API key - ${error.message}`); - } + const ruleType = context.ruleTypeRegistry.get(rule.attributes.alertTypeId); - const apiKeyAttributes = apiKeyAsAlertAttributes(createdAPIKey, username); + await ensureAuthorizationForBulkUpdate(context, operations, rule); - // collect generated API keys - if (apiKeyAttributes.apiKey) { - apiKeysMap.set(rule.id, { - ...apiKeysMap.get(rule.id), - newApiKey: apiKeyAttributes.apiKey, - }); - } + const { attributes, ruleActions, hasUpdateApiKeyOperation, isAttributesUpdateSkipped } = + await getUpdatedAttributesFromOperations(context, operations, rule, ruleType); + + validateScheduleInterval(context, attributes.schedule.interval, ruleType.id, rule.id); + + const { modifiedParams: ruleParams, isParamsUpdateSkipped } = paramsModifier + ? await paramsModifier(attributes.params as Params) + : { + modifiedParams: attributes.params as Params, + isParamsUpdateSkipped: true, + }; + + // If neither attributes nor parameters were updated, mark + // the rule as skipped and continue to the next rule. + if (isAttributesUpdateSkipped && isParamsUpdateSkipped) { + skipped.push({ + id: rule.id, + name: rule.attributes.name, + skip_reason: 'RULE_NOT_MODIFIED', + }); + return; + } + + // validate rule params + const validatedAlertTypeParams = validateRuleTypeParams(ruleParams, ruleType.validate?.params); + const validatedMutatedAlertTypeParams = validateMutatedRuleTypeParams( + validatedAlertTypeParams, + rule.attributes.params, + ruleType.validate?.params + ); + + const { + actions: rawAlertActions, + references, + params: updatedParams, + } = await extractReferences( + context, + ruleType, + ruleActions.actions, + validatedMutatedAlertTypeParams + ); + + const { apiKeyAttributes } = await prepareApiKeys( + context, + rule, + ruleType, + apiKeysMap, + attributes, + hasUpdateApiKeyOperation, + username + ); + + const { updatedAttributes } = updateAttributes( + context, + attributes, + apiKeyAttributes, + updatedParams, + rawAlertActions, + username + ); - // get notifyWhen - const notifyWhen = getRuleNotifyWhenType( - attributes.notifyWhen ?? null, - attributes.throttle ?? null - ); + rules.push({ + ...rule, + references, + attributes: updatedAttributes, + }); + } catch (error) { + errors.push({ + message: error.message, + rule: { + id: rule.id, + name: rule.attributes?.name, + }, + }); + context.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.BULK_EDIT, + error, + }) + ); + } +} + +async function ensureAuthorizationForBulkUpdate( + context: RulesClientContext, + operations: BulkEditOperation[], + rule: SavedObjectsFindResult +): Promise { + if (rule.attributes.actions.length === 0) { + return; + } + + for (const operation of operations) { + const { field } = operation; + if (field === 'snoozeSchedule' || field === 'apiKey') { + try { + await context.actionsAuthorization.ensureAuthorized('execute'); + break; + } catch (error) { + throw Error(`Rule not authorized for bulk ${field} update - ${error.message}`); + } + } + } +} - const updatedAttributes = updateMeta(context, { +async function getUpdatedAttributesFromOperations( + context: RulesClientContext, + operations: BulkEditOperation[], + rule: SavedObjectsFindResult, + ruleType: RuleType +) { + let attributes = cloneDeep(rule.attributes); + let ruleActions = { + actions: injectReferencesIntoActions(rule.id, rule.attributes.actions, rule.references || []), + }; + + let hasUpdateApiKeyOperation = false; + let isAttributesUpdateSkipped = true; + + for (const operation of operations) { + // Check if the update should be skipped for the current action. + // If it should, save the skip reasons in attributesUpdateSkipReasons + // and continue to the next operation before without + // the `isAttributesUpdateSkipped` flag to false. + switch (operation.field) { + case 'actions': { + await validateActions(context, ruleType, { ...attributes, actions: operation.value }); + + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + operation, + ruleActions + ); + if (isAttributeModified) { + ruleActions = modifiedAttributes; + isAttributesUpdateSkipped = false; + } + break; + } + case 'snoozeSchedule': { + // Silently skip adding snooze or snooze schedules on security + // rules until we implement snoozing of their rules + if (rule.attributes.consumer === AlertConsumers.SIEM) { + // While the rule is technically not updated, we are still marking + // the rule as updated in case of snoozing, until support + // for snoozing is added. + isAttributesUpdateSkipped = false; + break; + } + if (operation.operation === 'set') { + const snoozeAttributes = getBulkSnoozeAttributes(rule.attributes, operation.value); + try { + verifySnoozeScheduleLimit(snoozeAttributes); + } catch (error) { + throw Error(`Error updating rule: could not add snooze - ${error.message}`); + } + attributes = { ...attributes, - ...apiKeyAttributes, - params: updatedParams as RawRule['params'], - actions: rawAlertActions, - notifyWhen, - updatedBy: username, - updatedAt: new Date().toISOString(), - }); - - // add mapped_params - const mappedParams = getMappedParams(updatedParams); - - if (Object.keys(mappedParams).length) { - updatedAttributes.mapped_params = mappedParams; + ...snoozeAttributes, + }; + } + if (operation.operation === 'delete') { + const idsToDelete = operation.value && [...operation.value]; + if (idsToDelete?.length === 0) { + attributes.snoozeSchedule?.forEach((schedule) => { + if (schedule.id) { + idsToDelete.push(schedule.id); + } + }); } + attributes = { + ...attributes, + ...getBulkUnsnoozeAttributes(attributes, idsToDelete), + }; + } + isAttributesUpdateSkipped = false; + break; + } + case 'apiKey': { + hasUpdateApiKeyOperation = true; + isAttributesUpdateSkipped = false; + break; + } + default: { + const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation( + operation, + rule.attributes + ); - rules.push({ - ...rule, - references, - attributes: updatedAttributes, - }); - } catch (error) { - errors.push({ - message: error.message, - rule: { - id: rule.id, - name: rule.attributes?.name, - }, - }); - context.auditLogger?.log( - ruleAuditEvent({ - action: RuleAuditAction.BULK_EDIT, - error, - }) - ); + if (isAttributeModified) { + attributes = { + ...attributes, + ...modifiedAttributes, + }; + isAttributesUpdateSkipped = false; } - }, - { concurrency: API_KEY_GENERATE_CONCURRENCY } + } + } + } + return { + attributes, + ruleActions, + hasUpdateApiKeyOperation, + isAttributesUpdateSkipped, + }; +} + +function validateScheduleInterval( + context: RulesClientContext, + scheduleInterval: string, + ruleTypeId: string, + ruleId: string +): void { + if (!scheduleInterval) { + return; + } + const isIntervalInvalid = + parseDuration(scheduleInterval as string) < context.minimumScheduleIntervalInMs; + if (isIntervalInvalid && context.minimumScheduleInterval.enforce) { + throw Error( + `Error updating rule: the interval is less than the allowed minimum interval of ${context.minimumScheduleInterval.value}` + ); + } else if (isIntervalInvalid && !context.minimumScheduleInterval.enforce) { + context.logger.warn( + `Rule schedule interval (${scheduleInterval}) for "${ruleTypeId}" rule type with ID "${ruleId}" is less than the minimum value (${context.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent such changes.` ); } - await rulesFinder.close(); +} +async function prepareApiKeys( + context: RulesClientContext, + rule: SavedObjectsFindResult, + ruleType: RuleType, + apiKeysMap: ApiKeysMap, + attributes: RawRule, + hasUpdateApiKeyOperation: boolean, + username: string | null +): Promise<{ apiKeyAttributes: ApiKeyAttributes }> { + const shouldUpdateApiKey = attributes.enabled || hasUpdateApiKeyOperation; + + let createdAPIKey: CreateAPIKeyResult | null = null; + try { + createdAPIKey = shouldUpdateApiKey + ? await context.createAPIKey(generateAPIKeyName(ruleType.id, attributes.name)) + : null; + } catch (error) { + throw Error(`Error updating rule: could not create API key - ${error.message}`); + } + + const apiKeyAttributes = apiKeyAsAlertAttributes(createdAPIKey, username); + // collect generated API keys + if (apiKeyAttributes.apiKey) { + apiKeysMap.set(rule.id, { + ...apiKeysMap.get(rule.id), + newApiKey: apiKeyAttributes.apiKey, + }); + } + + return { + apiKeyAttributes, + }; +} + +function updateAttributes( + context: RulesClientContext, + attributes: RawRule, + apiKeyAttributes: ApiKeyAttributes, + updatedParams: RuleTypeParams, + rawAlertActions: RawRuleAction[], + username: string | null +): { + updatedAttributes: RawRule; +} { + // get notifyWhen + const notifyWhen = getRuleNotifyWhenType( + attributes.notifyWhen ?? null, + attributes.throttle ?? null + ); + + const updatedAttributes = updateMeta(context, { + ...attributes, + ...apiKeyAttributes, + params: updatedParams as RawRule['params'], + actions: rawAlertActions, + notifyWhen, + updatedBy: username, + updatedAt: new Date().toISOString(), + }); + + // add mapped_params + const mappedParams = getMappedParams(updatedParams); + + if (Object.keys(mappedParams).length) { + updatedAttributes.mapped_params = mappedParams; + } + + return { + updatedAttributes, + }; +} + +async function saveBulkUpdatedRules( + context: RulesClientContext, + rules: Array>, + apiKeysMap: ApiKeysMap +) { + const apiKeysToInvalidate: string[] = []; let result; try { result = await context.unsecuredSavedObjectsClient.bulkCreate(rules, { overwrite: true }); @@ -514,12 +728,9 @@ async function bulkEditOcc( if (apiKeysMap.size > 0) { await bulkMarkApiKeysForInvalidation( { - apiKeys: Array.from(apiKeysMap.values()).reduce((acc, value) => { - if (value.newApiKey) { - acc.push(value.newApiKey); - } - return acc; - }, []), + apiKeys: Array.from(apiKeysMap.values()) + .filter((value) => value.newApiKey) + .map((value) => value.newApiKey as string), }, context.logger, context.unsecuredSavedObjectsClient @@ -541,5 +752,5 @@ async function bulkEditOcc( } }); - return { apiKeysToInvalidate, resultSavedObjects: result.saved_objects, errors, rules }; + return { result, apiKeysToInvalidate }; } diff --git a/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts index 5e440d2e6b6d7..652f243060c82 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts @@ -59,6 +59,9 @@ const rulesClientParams: jest.Mocked = { auditLogger, minimumScheduleInterval: { value: '1m', enforce: false }, }; +const paramsModifier = jest.fn(); + +const MOCK_API_KEY = Buffer.from('123:abc').toString('base64'); beforeEach(() => { getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry); @@ -93,7 +96,7 @@ describe('bulkEdit()', () => { ...existingRule, attributes: { ...existingRule.attributes, - apiKey: Buffer.from('123:abc').toString('base64'), + apiKey: MOCK_API_KEY, }, }; @@ -129,7 +132,14 @@ describe('bulkEdit()', () => { total: 1, }); - mockCreatePointInTimeFinderAsInternalUser(); + mockCreatePointInTimeFinderAsInternalUser({ + saved_objects: [ + { + ...existingDecryptedRule, + attributes: { ...existingDecryptedRule.attributes, enabled: true }, + }, + ], + }); unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: [existingRule], @@ -150,7 +160,19 @@ describe('bulkEdit()', () => { producer: 'alerts', }); }); + describe('tags operations', () => { + beforeEach(() => { + mockCreatePointInTimeFinderAsInternalUser({ + saved_objects: [ + { + ...existingDecryptedRule, + attributes: { ...existingDecryptedRule.attributes, tags: ['foo'] }, + }, + ], + }); + }); + test('should add new tag', async () => { unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: [ @@ -229,7 +251,6 @@ describe('bulkEdit()', () => { }, ], }); - const result = await rulesClient.bulkEdit({ filter: '', operations: [ @@ -309,6 +330,256 @@ describe('bulkEdit()', () => { { overwrite: true } ); }); + + test('should skip operation when adding already existing tags', async () => { + const result = await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + field: 'tags', + operation: 'add', + value: ['foo'], + }, + ], + }); + + expect(result.total).toBe(1); + expect(result.errors).toHaveLength(0); + expect(result.rules).toHaveLength(0); + expect(result.skipped).toHaveLength(1); + expect(result.skipped[0]).toHaveProperty('id', existingRule.id); + expect(result.skipped[0]).toHaveProperty('skip_reason', 'RULE_NOT_MODIFIED'); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0); + expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(0); + }); + + test('should skip operation when adding no tags', async () => { + const result = await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + field: 'tags', + operation: 'add', + value: [], + }, + ], + }); + + expect(result.total).toBe(1); + expect(result.errors).toHaveLength(0); + expect(result.rules).toHaveLength(0); + expect(result.skipped).toHaveLength(1); + expect(result.skipped[0]).toHaveProperty('id', existingRule.id); + expect(result.skipped[0]).toHaveProperty('skip_reason', 'RULE_NOT_MODIFIED'); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0); + expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(0); + }); + + test('should skip operation when deleting non existing tags', async () => { + const result = await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + field: 'tags', + operation: 'delete', + value: ['bar'], + }, + ], + }); + + expect(result.total).toBe(1); + expect(result.errors).toHaveLength(0); + expect(result.rules).toHaveLength(0); + expect(result.skipped).toHaveLength(1); + expect(result.skipped[0]).toHaveProperty('id', existingRule.id); + expect(result.skipped[0]).toHaveProperty('skip_reason', 'RULE_NOT_MODIFIED'); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0); + expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(0); + }); + + test('should skip operation when deleting no tags', async () => { + const result = await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + field: 'tags', + operation: 'delete', + value: [], + }, + ], + }); + + expect(result.total).toBe(1); + expect(result.errors).toHaveLength(0); + expect(result.rules).toHaveLength(0); + expect(result.skipped).toHaveLength(1); + expect(result.skipped[0]).toHaveProperty('id', existingRule.id); + expect(result.skipped[0]).toHaveProperty('skip_reason', 'RULE_NOT_MODIFIED'); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0); + expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(0); + }); + }); + + describe('index pattern operations', () => { + beforeEach(() => { + mockCreatePointInTimeFinderAsInternalUser({ + saved_objects: [ + { + ...existingDecryptedRule, + attributes: { + ...existingDecryptedRule.attributes, + params: { index: ['index-1', 'index-2'] }, + }, + }, + ], + }); + }); + + test('should add index patterns', async () => { + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ + saved_objects: [ + { + id: '1', + type: 'alert', + attributes: { + enabled: true, + tags: ['foo'], + alertTypeId: 'myType', + schedule: { interval: '1m' }, + consumer: 'myApp', + scheduledTaskId: 'task-123', + params: { + index: ['test-1', 'test-2', 'test-4', 'test-5'], + }, + throttle: null, + notifyWhen: null, + actions: [], + }, + references: [], + version: '123', + }, + ], + }); + + paramsModifier.mockResolvedValue({ + modifiedParams: { + index: ['test-1', 'test-2', 'test-4', 'test-5'], + }, + isParamsUpdateSkipped: false, + }); + + const result = await rulesClient.bulkEdit({ + filter: '', + operations: [], + paramsModifier, + }); + + expect(result.rules[0].params).toHaveProperty('index', [ + 'test-1', + 'test-2', + 'test-4', + 'test-5', + ]); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + expect.objectContaining({ + id: '1', + type: 'alert', + attributes: expect.objectContaining({ + params: expect.objectContaining({ + index: ['test-1', 'test-2', 'test-4', 'test-5'], + }), + }), + }), + ], + { overwrite: true } + ); + }); + + test('should delete index patterns', async () => { + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ + saved_objects: [ + { + id: '1', + type: 'alert', + attributes: { + enabled: true, + tags: ['foo'], + alertTypeId: 'myType', + schedule: { interval: '1m' }, + consumer: 'myApp', + scheduledTaskId: 'task-123', + params: { + index: ['test-1'], + }, + throttle: null, + notifyWhen: null, + actions: [], + }, + references: [], + version: '123', + }, + ], + }); + + paramsModifier.mockResolvedValue({ + modifiedParams: { + index: ['test-1'], + }, + isParamsUpdateSkipped: false, + }); + + const result = await rulesClient.bulkEdit({ + filter: '', + operations: [], + paramsModifier, + }); + + expect(result.rules[0].params).toHaveProperty('index', ['test-1']); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + expect.objectContaining({ + id: '1', + type: 'alert', + attributes: expect.objectContaining({ + params: expect.objectContaining({ + index: ['test-1'], + }), + }), + }), + ], + { overwrite: true } + ); + }); + + test('should skip operation when params modifiers does not modify index pattern array', async () => { + paramsModifier.mockResolvedValue({ + modifiedParams: { + index: ['test-1', 'test-2'], + }, + isParamsUpdateSkipped: true, + }); + + const result = await rulesClient.bulkEdit({ + filter: '', + operations: [], + paramsModifier, + }); + + expect(result.rules).toHaveLength(0); + expect(result.skipped[0].id).toBe(existingRule.id); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0); + expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(0); + }); }); describe('snoozeSchedule operations', () => { @@ -714,6 +985,16 @@ describe('bulkEdit()', () => { }); describe('apiKey operations', () => { + beforeEach(() => { + mockCreatePointInTimeFinderAsInternalUser({ + saved_objects: [ + { + ...existingDecryptedRule, + attributes: { ...existingDecryptedRule.attributes, tags: ['foo'] }, + }, + ], + }); + }); test('should bulk update API key', async () => { // Does not generate API key for disabled rules await rulesClient.bulkEdit({ @@ -744,6 +1025,252 @@ describe('bulkEdit()', () => { }); }); + describe('mixed operations', () => { + beforeEach(() => { + mockCreatePointInTimeFinderAsInternalUser({ + saved_objects: [ + { + ...existingDecryptedRule, + attributes: { + ...existingDecryptedRule.attributes, + tags: ['foo'], + params: { index: ['index-1', 'index-2'] }, + }, + }, + ], + }); + }); + + it('should succesfully update tags and index patterns and return updated rule', async () => { + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ + saved_objects: [ + { + id: '1', + type: 'alert', + attributes: { + enabled: true, + tags: ['foo', 'test-1'], + alertTypeId: 'myType', + schedule: { interval: '1m' }, + consumer: 'myApp', + scheduledTaskId: 'task-123', + params: { + index: ['index-1', 'index-2', 'index-3'], + }, + throttle: null, + notifyWhen: null, + actions: [], + }, + references: [], + version: '123', + }, + ], + }); + + paramsModifier.mockResolvedValue({ + modifiedParams: { + index: ['index-1', 'index-2', 'index-3'], + }, + isParamsUpdateSkipped: false, + }); + + const result = await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + field: 'tags', + operation: 'add', + value: ['test-1'], + }, + ], + paramsModifier, + }); + + expect(result.rules[0]).toHaveProperty('tags', ['foo', 'test-1']); + expect(result.rules[0]).toHaveProperty('params.index', ['index-1', 'index-2', 'index-3']); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + expect.objectContaining({ + id: '1', + type: 'alert', + attributes: expect.objectContaining({ + tags: ['foo', 'test-1'], + params: { + index: ['index-1', 'index-2', 'index-3'], + }, + }), + }), + ], + { overwrite: true } + ); + }); + + it('should succesfully update rule if tags are updated but index patterns are not', async () => { + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ + saved_objects: [ + { + id: '1', + type: 'alert', + attributes: { + enabled: true, + tags: ['foo', 'test-1'], + alertTypeId: 'myType', + schedule: { interval: '1m' }, + consumer: 'myApp', + scheduledTaskId: 'task-123', + params: { + index: ['index-1', 'index-2'], + }, + throttle: null, + notifyWhen: null, + actions: [], + }, + references: [], + version: '123', + }, + ], + }); + + paramsModifier.mockResolvedValue({ + modifiedParams: { + index: ['index-1', 'index-2'], + }, + isParamsUpdateSkipped: true, + }); + + const result = await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + field: 'tags', + operation: 'add', + value: ['test-1'], + }, + ], + paramsModifier, + }); + + expect(result.rules[0]).toHaveProperty('tags', ['foo', 'test-1']); + expect(result.rules[0]).toHaveProperty('params.index', ['index-1', 'index-2']); + expect(result.skipped).toHaveLength(0); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + expect.objectContaining({ + id: '1', + type: 'alert', + attributes: expect.objectContaining({ + tags: ['foo', 'test-1'], + params: { + index: ['index-1', 'index-2'], + }, + }), + }), + ], + { overwrite: true } + ); + }); + + it('should succesfully update rule if index patterns are updated but tags are not', async () => { + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ + saved_objects: [ + { + id: '1', + type: 'alert', + attributes: { + enabled: true, + tags: ['foo'], + alertTypeId: 'myType', + schedule: { interval: '1m' }, + consumer: 'myApp', + scheduledTaskId: 'task-123', + params: { + index: ['index-1', 'index-2', 'index-3'], + }, + throttle: null, + notifyWhen: null, + actions: [], + }, + references: [], + version: '123', + }, + ], + }); + + paramsModifier.mockResolvedValue({ + modifiedParams: { + index: ['index-1', 'index-2', 'index-3'], + }, + isParamsUpdateSkipped: false, + }); + + const result = await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + field: 'tags', + operation: 'add', + value: ['foo'], + }, + ], + paramsModifier, + }); + + expect(result.rules[0]).toHaveProperty('tags', ['foo']); + expect(result.rules[0]).toHaveProperty('params.index', ['index-1', 'index-2', 'index-3']); + expect(result.skipped).toHaveLength(0); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( + [ + expect.objectContaining({ + id: '1', + type: 'alert', + attributes: expect.objectContaining({ + tags: ['foo'], + params: { + index: ['index-1', 'index-2', 'index-3'], + }, + }), + }), + ], + { overwrite: true } + ); + }); + + it('should skip rule update if neither index patterns nor tags are updated', async () => { + paramsModifier.mockResolvedValue({ + modifiedParams: { + index: ['index-1', 'index-2'], + }, + isParamsUpdateSkipped: true, + }); + + const result = await rulesClient.bulkEdit({ + filter: '', + operations: [ + { + field: 'tags', + operation: 'add', + value: ['foo'], + }, + ], + paramsModifier, + }); + + expect(result.skipped[0]).toHaveProperty('id', existingRule.id); + expect(result.skipped[0]).toHaveProperty('skip_reason', 'RULE_NOT_MODIFIED'); + + expect(result.rules).toHaveLength(0); + + expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0); + expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(0); + }); + }); + describe('ruleTypes aggregation and validation', () => { test('should call unsecuredSavedObjectsClient.find for aggregations by alertTypeId and consumer', async () => { await rulesClient.bulkEdit({ @@ -964,6 +1491,18 @@ describe('bulkEdit()', () => { }); describe('apiKeys', () => { + beforeEach(() => { + createAPIKeyMock.mockResolvedValueOnce({ apiKeysEnabled: true, result: { api_key: '111' } }); + mockCreatePointInTimeFinderAsInternalUser({ + saved_objects: [ + { + ...existingDecryptedRule, + attributes: { ...existingDecryptedRule.attributes, enabled: true }, + }, + ], + }); + }); + test('should call createPointInTimeFinderDecryptedAsInternalUser that returns api Keys', async () => { await rulesClient.bulkEdit({ filter: 'alert.attributes.tags: "APM"', @@ -1021,16 +1560,6 @@ describe('bulkEdit()', () => { }); test('should call bulkMarkApiKeysForInvalidation to invalidate unused keys if bulkCreate failed', async () => { - createAPIKeyMock.mockReturnValue({ apiKeysEnabled: true, result: { api_key: '111' } }); - mockCreatePointInTimeFinderAsInternalUser({ - saved_objects: [ - { - ...existingDecryptedRule, - attributes: { ...existingDecryptedRule.attributes, enabled: true }, - }, - ], - }); - unsecuredSavedObjectsClient.bulkCreate.mockImplementation(() => { throw new Error('Fail'); }); @@ -1057,16 +1586,6 @@ describe('bulkEdit()', () => { }); test('should call bulkMarkApiKeysForInvalidation to invalidate unused keys if SO update failed', async () => { - createAPIKeyMock.mockReturnValue({ apiKeysEnabled: true, result: { api_key: '111' } }); - mockCreatePointInTimeFinderAsInternalUser({ - saved_objects: [ - { - ...existingDecryptedRule, - attributes: { ...existingDecryptedRule.attributes, enabled: true }, - }, - ], - }); - unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: [ { @@ -1128,15 +1647,6 @@ describe('bulkEdit()', () => { }); test('should return error in rule errors if key is not generated', async () => { - mockCreatePointInTimeFinderAsInternalUser({ - saved_objects: [ - { - ...existingDecryptedRule, - attributes: { ...existingDecryptedRule.attributes, enabled: true }, - }, - ], - }); - await rulesClient.bulkEdit({ filter: 'alert.attributes.tags: "APM"', operations: [ @@ -1152,6 +1662,12 @@ describe('bulkEdit()', () => { }); describe('params validation', () => { + beforeEach(() => { + mockCreatePointInTimeFinderAsInternalUser({ + saved_objects: [existingDecryptedRule], + }); + }); + test('should return error for rule that failed params validation', async () => { ruleTypeRegistry.get.mockReturnValue({ id: '123', @@ -1218,7 +1734,7 @@ describe('bulkEdit()', () => { { field: 'tags', operation: 'add', - value: ['test-1'], + value: ['test-1', 'another-tag'], }, ], }); @@ -1251,7 +1767,7 @@ describe('bulkEdit()', () => { paramsModifier: async (params) => { params.index = ['test-index-*']; - return params; + return { modifiedParams: params, isParamsUpdateSkipped: false, skipReasons: [] }; }, }); @@ -1294,7 +1810,7 @@ describe('bulkEdit()', () => { paramsModifier: async (params) => { params.index = ['test-index-*']; - return params; + return { modifiedParams: params, isParamsUpdateSkipped: false, skipReasons: [] }; }, }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts index d595dc88441cc..b0860c55ebd5a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts @@ -66,7 +66,9 @@ const BulkActionEditPayloadTags = t.type({ value: RuleTagArray, }); -type BulkActionEditPayloadIndexPatterns = t.TypeOf; +export type BulkActionEditPayloadIndexPatterns = t.TypeOf< + typeof BulkActionEditPayloadIndexPatterns +>; const BulkActionEditPayloadIndexPatterns = t.intersection([ t.type({ type: t.union([ diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/response_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/response_schema.ts new file mode 100644 index 0000000000000..e9b4ad6bbc1e5 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/response_schema.ts @@ -0,0 +1,53 @@ +/* + * 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 type { BulkActionSkipResult } from '@kbn/alerting-plugin/common'; +import type { RuleResponse } from '../../../../rule_schema'; +import type { BulkActionsDryRunErrCode } from '../../../../../constants'; + +export interface RuleDetailsInError { + id: string; + name?: string; +} +export interface NormalizedRuleError { + message: string; + status_code: number; + err_code?: BulkActionsDryRunErrCode; + rules: RuleDetailsInError[]; +} +export interface BulkEditActionResults { + updated: RuleResponse[]; + created: RuleResponse[]; + deleted: RuleResponse[]; + skipped: BulkActionSkipResult[]; +} + +export interface BulkEditActionSummary { + failed: number; + skipped: number; + succeeded: number; + total: number; +} +export interface BulkEditActionSuccessResponse { + success: boolean; + rules_count: number; + attributes: { + results: BulkEditActionResults; + summary: BulkEditActionSummary; + }; +} +export interface BulkEditActionErrorResponse { + status_code: number; + message: string; + attributes: { + results: BulkEditActionResults; + summary: BulkEditActionSummary; + errors?: NormalizedRuleError[]; + }; +} + +export type BulkEditActionResponse = BulkEditActionSuccessResponse | BulkEditActionErrorResponse; diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules.cy.ts index 701aed857a7ea..7aa90997a8101 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules.cy.ts @@ -10,7 +10,6 @@ import { MODAL_CONFIRMATION_BODY, RULE_CHECKBOX, RULES_TAGS_POPOVER_BTN, - TOASTER_BODY, MODAL_ERROR_BODY, } from '../../screens/alerts_detection_rules'; @@ -203,7 +202,7 @@ describe('Detection rules, bulk edit', () => { // action should finish typeTags(['test-tag']); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfCustomRulesToBeEdited }); }); it('Prebuilt and custom rules selected: user cancels action', () => { @@ -230,9 +229,9 @@ describe('Detection rules, bulk edit', () => { // open add tags form and add 2 new tags openBulkEditAddTagsForm(); - typeTags(prePopulatedTags); + typeTags(['new-tag-1']); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount }); + waitForBulkEditActionToFinish({ updatedCount: rulesCount }); testMultipleSelectedRulesLabel(rulesCount); // check if first four(rulesCount) rules still selected and tags are updated @@ -241,7 +240,7 @@ describe('Detection rules, bulk edit', () => { cy.get(RULES_TAGS_POPOVER_BTN) .eq(i) .each(($el) => { - testTagsBadge($el, prePopulatedTags); + testTagsBadge($el, prePopulatedTags.concat(['new-tag-1'])); }); } }); @@ -280,7 +279,7 @@ describe('Detection rules, bulk edit', () => { openBulkEditAddTagsForm(); typeTags(tagsToBeAdded); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfCustomRulesToBeEdited }); // check if all rules have been updated with new tags testAllTagsBadges(resultingTags); @@ -309,12 +308,7 @@ describe('Detection rules, bulk edit', () => { openBulkEditAddTagsForm(); typeTags(tagsToBeAdded); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); - - cy.get(TOASTER_BODY).should( - 'have.text', - `You've successfully updated ${expectedNumberOfCustomRulesToBeEdited} rules` - ); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfCustomRulesToBeEdited }); }); it('Overwrite tags in custom rules', () => { @@ -336,7 +330,7 @@ describe('Detection rules, bulk edit', () => { typeTags(tagsToOverwrite); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfCustomRulesToBeEdited }); // check if all rules have been updated with new tags testAllTagsBadges(tagsToOverwrite); @@ -358,7 +352,7 @@ describe('Detection rules, bulk edit', () => { openBulkEditDeleteTagsForm(); typeTags(tagsToDelete); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfCustomRulesToBeEdited }); // check tags has been removed from all rules testAllTagsBadges(resultingTags); @@ -383,7 +377,7 @@ describe('Detection rules, bulk edit', () => { typeIndexPatterns(indexPattersToBeAdded); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfNotMLRules }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfNotMLRules }); // check if rule has been updated goToTheRuleDetailsOf(RULE_NAME); @@ -413,7 +407,7 @@ describe('Detection rules, bulk edit', () => { typeIndexPatterns(indexPattersToBeAdded); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfNotMLRules }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfNotMLRules }); // check if rule has been updated goToTheRuleDetailsOf(RULE_NAME); @@ -431,12 +425,9 @@ describe('Detection rules, bulk edit', () => { typeIndexPatterns(indexPattersToBeAdded); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfNotMLRules }); - - cy.get(TOASTER_BODY).should( - 'have.text', - `You've successfully updated ${expectedNumberOfNotMLRules} rules. If you did not select to apply changes to rules using Kibana data views, those rules were not updated and will continue using data views.` - ); + waitForBulkEditActionToFinish({ + updatedCount: expectedNumberOfNotMLRules, + }); }); it('Overwrite index patterns in custom rules', () => { @@ -458,7 +449,7 @@ describe('Detection rules, bulk edit', () => { typeIndexPatterns(indexPattersToWrite); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfNotMLRules }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfNotMLRules }); // check if rule has been updated goToTheRuleDetailsOf(RULE_NAME); @@ -477,7 +468,7 @@ describe('Detection rules, bulk edit', () => { typeIndexPatterns(indexPatternsToDelete); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfNotMLRules }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfNotMLRules }); // check if rule has been updated goToTheRuleDetailsOf(RULE_NAME); @@ -494,7 +485,7 @@ describe('Detection rules, bulk edit', () => { submitBulkEditForm(); // error toast should be displayed that that rules edit failed - cy.contains(TOASTER_BODY, `${expectedNumberOfNotMLRules} rules failed to update.`); + waitForBulkEditActionToFinish({ failedCount: expectedNumberOfNotMLRules }); // on error toast button click display error that index patterns can't be empty clickErrorToastBtn(); @@ -520,7 +511,7 @@ describe('Detection rules, bulk edit', () => { selectTimelineTemplate(timelineTemplateName); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfCustomRulesToBeEdited }); // check if timeline template has been updated to selected one goToTheRuleDetailsOf(RULE_NAME); @@ -536,7 +527,7 @@ describe('Detection rules, bulk edit', () => { clickApplyTimelineTemplatesMenuItem(); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfCustomRulesToBeEdited }); // check if timeline template has been updated to selected one, by opening rule that have had timeline prior to editing goToTheRuleDetailsOf(RULE_NAME); @@ -570,7 +561,7 @@ describe('Detection rules, bulk edit', () => { setScheduleLookbackTimeUnit('Minutes'); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfCustomRulesToBeEdited }); goToTheRuleDetailsOf(RULE_NAME); @@ -592,7 +583,7 @@ describe('Detection rules, bulk edit', () => { setScheduleLookbackTimeUnit('Seconds'); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfCustomRulesToBeEdited }); goToTheRuleDetailsOf(RULE_NAME); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts index b897aa3024cda..8de71890326d6 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts @@ -150,7 +150,7 @@ describe('Detection rules, bulk edit of rule actions', () => { addSlackRuleAction(expectedSlackMessage); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfRulesToBeEdited }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfRulesToBeEdited }); // check if rule has been updated goToEditRuleActionsSettingsOf(ruleNameToAssert); @@ -181,7 +181,7 @@ describe('Detection rules, bulk edit of rule actions', () => { ); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfRulesToBeEdited }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfRulesToBeEdited }); // check if rule has been updated goToEditRuleActionsSettingsOf(ruleNameToAssert); @@ -204,7 +204,7 @@ describe('Detection rules, bulk edit of rule actions', () => { addEmailConnectorAndRuleAction(expectedEmail, expectedSubject); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfCustomRulesToBeEdited }); // check if rule has been updated goToEditRuleActionsSettingsOf(ruleNameToAssert); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_data_view.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_data_view.cy.ts index 766c8d9483f0a..4644ed8a436d4 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_data_view.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_data_view.cy.ts @@ -58,13 +58,13 @@ const expectedIndexPatterns = ['index-1-*', 'index-2-*']; const expectedNumberOfCustomRulesToBeEdited = 6; -const indexDataSource = { dataView: DATA_VIEW_ID, type: 'dataView' } as const; +const dataViewDataSource = { dataView: DATA_VIEW_ID, type: 'dataView' } as const; -const defaultRuleData = { - dataSource: indexDataSource, +const dataViewRuleData = { + dataSource: dataViewDataSource, }; -describe('Detection rules, bulk edit, data view', () => { +describe('Bulk editing index patterns of rules with a data view only', () => { before(() => { cleanKibana(); login(); @@ -75,26 +75,29 @@ describe('Detection rules, bulk edit, data view', () => { postDataView(DATA_VIEW_ID); - createCustomRule({ ...getNewRule(), ...defaultRuleData }, '1'); - createEventCorrelationRule({ ...getEqlRule(), ...defaultRuleData }, '2'); - createCustomIndicatorRule({ ...getNewThreatIndicatorRule(), ...defaultRuleData }, '3'); - createThresholdRule({ ...getNewThresholdRule(), ...defaultRuleData }, '4'); - createNewTermsRule({ ...getNewTermsRule(), ...defaultRuleData }, '5'); - createSavedQueryRule({ ...getNewRule(), ...defaultRuleData, savedId: 'mocked' }, '6'); + createCustomRule({ ...getNewRule(), ...dataViewRuleData }, '1'); + createEventCorrelationRule({ ...getEqlRule(), ...dataViewRuleData }, '2'); + createCustomIndicatorRule({ ...getNewThreatIndicatorRule(), ...dataViewRuleData }, '3'); + createThresholdRule({ ...getNewThresholdRule(), ...dataViewRuleData }, '4'); + createNewTermsRule({ ...getNewTermsRule(), ...dataViewRuleData }, '5'); + createSavedQueryRule({ ...getNewRule(), ...dataViewRuleData, savedId: 'mocked' }, '6'); visitWithoutDateRange(SECURITY_DETECTIONS_RULES_URL); waitForRulesTableToBeLoaded(); }); - it('Add index patterns to custom rules with configured data view', () => { + it('Add index patterns to custom rules with configured data view: all rules are skipped', () => { selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); openBulkEditAddIndexPatternsForm(); typeIndexPatterns(expectedIndexPatterns); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + waitForBulkEditActionToFinish({ + skippedCount: expectedNumberOfCustomRulesToBeEdited, + showDataViewsWarning: true, + }); // check if rule still has data view and index patterns field does not exist goToRuleDetails(); @@ -102,7 +105,7 @@ describe('Detection rules, bulk edit, data view', () => { assertDetailsNotExist(INDEX_PATTERNS_DETAILS); }); - it('Add index patterns to custom rules with configured data view when data view checkbox is checked', () => { + it('Add index patterns to custom rules with configured data view when data view checkbox is checked: rules are updated', () => { selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); openBulkEditAddIndexPatternsForm(); @@ -115,7 +118,7 @@ describe('Detection rules, bulk edit, data view', () => { submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfCustomRulesToBeEdited }); // check if rule has been updated with index patterns and data view does not exist goToRuleDetails(); @@ -123,7 +126,7 @@ describe('Detection rules, bulk edit, data view', () => { assertDetailsNotExist(DATA_VIEW_DETAILS); }); - it('Overwrite index patterns in custom rules with configured data view', () => { + it('Overwrite index patterns in custom rules with configured data view when overwrite data view checkbox is NOT checked:: rules are skipped', () => { selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); openBulkEditAddIndexPatternsForm(); @@ -131,7 +134,10 @@ describe('Detection rules, bulk edit, data view', () => { checkOverwriteIndexPatternsCheckbox(); submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + waitForBulkEditActionToFinish({ + skippedCount: expectedNumberOfCustomRulesToBeEdited, + showDataViewsWarning: true, + }); // check if rule still has data view and index patterns field does not exist goToRuleDetails(); @@ -139,7 +145,7 @@ describe('Detection rules, bulk edit, data view', () => { assertDetailsNotExist(INDEX_PATTERNS_DETAILS); }); - it('Overwrite index patterns in custom rules with configured data view when data view checkbox is checked', () => { + it('Overwrite index patterns in custom rules with configured data view when overwrite data view checkbox is checked: rules are updated', () => { selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); openBulkEditAddIndexPatternsForm(); @@ -149,7 +155,7 @@ describe('Detection rules, bulk edit, data view', () => { submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfCustomRulesToBeEdited }); // check if rule has been overwritten with index patterns and data view does not exist goToRuleDetails(); @@ -157,7 +163,7 @@ describe('Detection rules, bulk edit, data view', () => { assertDetailsNotExist(DATA_VIEW_DETAILS); }); - it('Delete index patterns in custom rules with configured data view', () => { + it('Delete index patterns in custom rules with configured data view: rules are skipped', () => { selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); openBulkEditDeleteIndexPatternsForm(); @@ -168,10 +174,79 @@ describe('Detection rules, bulk edit, data view', () => { submitBulkEditForm(); - waitForBulkEditActionToFinish({ rulesCount: expectedNumberOfCustomRulesToBeEdited }); + waitForBulkEditActionToFinish({ + skippedCount: expectedNumberOfCustomRulesToBeEdited, + showDataViewsWarning: true, + }); // check if rule still has data view and index patterns field does not exist goToRuleDetails(); getDetails(DATA_VIEW_DETAILS).contains(DATA_VIEW_ID); }); }); + +describe('Bulk editing index patterns of rules with index patterns and rules with a data view', () => { + const customRulesNumber = 2; + before(() => { + cleanKibana(); + login(); + }); + beforeEach(() => { + deleteAlertsAndRules(); + esArchiverResetKibana(); + + postDataView(DATA_VIEW_ID); + + createCustomRule({ ...getNewRule(), ...dataViewRuleData }, '1'); + createCustomRule( + { + ...getNewRule(), + dataSource: { + type: 'indexPatterns', + index: ['test-index-1-*'], + }, + }, + '2' + ); + + visitWithoutDateRange(SECURITY_DETECTIONS_RULES_URL); + + waitForRulesTableToBeLoaded(); + }); + + it('Add index patterns to custom rules: one rule is updated, one rule is skipped', () => { + selectNumberOfRules(customRulesNumber); + + openBulkEditAddIndexPatternsForm(); + typeIndexPatterns(expectedIndexPatterns); + submitBulkEditForm(); + + waitForBulkEditActionToFinish({ + updatedCount: 1, + skippedCount: 1, + showDataViewsWarning: true, + }); + + // check if rule still has data view and index patterns field does not exist + goToRuleDetails(); + getDetails(DATA_VIEW_DETAILS).contains(DATA_VIEW_ID); + assertDetailsNotExist(INDEX_PATTERNS_DETAILS); + }); + + it('Add index patterns to custom rules when overwrite data view checkbox is checked: all rules are updated', () => { + selectNumberOfRules(customRulesNumber); + + openBulkEditAddIndexPatternsForm(); + typeIndexPatterns(expectedIndexPatterns); + checkOverwriteDataViewCheckbox(); + submitBulkEditForm(); + + waitForBulkEditActionToFinish({ + updatedCount: 2, + }); + + // check if rule still has data view and index patterns field does not exist + goToRuleDetails(); + assertDetailsNotExist(DATA_VIEW_DETAILS); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts index 64f0e88849356..e01e72fe140db 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts @@ -125,9 +125,42 @@ export const openTagsSelect = () => { export const submitBulkEditForm = () => cy.get(RULES_BULK_EDIT_FORM_CONFIRM_BTN).click(); -export const waitForBulkEditActionToFinish = ({ rulesCount }: { rulesCount: number }) => { +export const waitForBulkEditActionToFinish = ({ + updatedCount, + skippedCount, + failedCount, + showDataViewsWarning = false, +}: { + updatedCount?: number; + skippedCount?: number; + failedCount?: number; + showDataViewsWarning?: boolean; +}) => { cy.get(BULK_ACTIONS_PROGRESS_BTN).should('be.disabled'); - cy.contains(TOASTER_BODY, `You've successfully updated ${rulesCount} rule`); + + if (updatedCount !== undefined) { + cy.contains(TOASTER_BODY, `You've successfully updated ${updatedCount} rule`); + } + if (failedCount !== undefined) { + if (failedCount === 1) { + cy.contains(TOASTER_BODY, `${failedCount} rule failed to update`); + } else { + cy.contains(TOASTER_BODY, `${failedCount} rules failed to update`); + } + } + if (skippedCount !== undefined) { + if (skippedCount === 1) { + cy.contains(TOASTER_BODY, `${skippedCount} rule was skipped`); + } else { + cy.contains(TOASTER_BODY, `${skippedCount} rules were skipped`); + } + if (showDataViewsWarning) { + cy.contains( + TOASTER_BODY, + 'If you did not select to apply changes to rules using Kibana data views, those rules were not updated and will continue using data views.' + ); + } + } }; export const checkPrebuiltRulesCannotBeModified = (rulesCount: number) => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts index 9e4f22484a00d..c867b6b3fd792 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts @@ -189,6 +189,7 @@ export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise 0 ? ` ${i18n.RULES_BULK_EDIT_SUCCESS_DATA_VIEW_RULES_SKIPPED_DETAIL}` : null; if ( editPayload.some( (x) => @@ -70,12 +72,13 @@ export function explainBulkEditSuccess( x.type === BulkActionEditType.delete_index_patterns ) ) { - return `${i18n.RULES_BULK_EDIT_SUCCESS_DESCRIPTION(summary.succeeded)}. ${ - i18n.RULES_BULK_EDIT_SUCCESS_INDEX_EDIT_DESCRIPTION - }`; + return `${i18n.RULES_BULK_EDIT_SUCCESS_DESCRIPTION( + summary.succeeded, + summary.skipped + )}${dataViewSkipDetail}`; } - return i18n.RULES_BULK_EDIT_SUCCESS_DESCRIPTION(summary.succeeded); + return i18n.RULES_BULK_EDIT_SUCCESS_DESCRIPTION(summary.succeeded, summary.skipped); } export function summarizeBulkError(action: BulkActionType): string { @@ -125,7 +128,7 @@ export function explainBulkError(action: BulkActionType, error: HTTPError): stri return i18n.RULES_BULK_DISABLE_FAILURE_DESCRIPTION(summary.failed); case BulkActionType.edit: - return i18n.RULES_BULK_EDIT_FAILURE_DESCRIPTION(summary.failed); + return i18n.RULES_BULK_EDIT_FAILURE_DESCRIPTION(summary.failed, summary.skipped); } } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/helpers.ts index 13666f514b0be..49d29fc34da76 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/helpers.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/helpers.ts @@ -69,6 +69,7 @@ export const getExportedRulesCounts = async (blob: Blob): Promise +export const RULES_BULK_EDIT_SUCCESS_DESCRIPTION = ( + succeededRulesCount: number, + skippedRulesCount: number +) => i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.successToastDescription', { - values: { rulesCount }, - defaultMessage: - "You've successfully updated {rulesCount, plural, =1 {# rule} other {# rules}}", + values: { succeededRulesCount, skippedRulesCount }, + defaultMessage: `{succeededRulesCount, plural, =0 {} =1 {You've successfully updated # rule. } other {You've successfully updated # rules. }} + {skippedRulesCount, plural, =0 {} =1 { # rule was skipped.} other { # rules were skipped.}} + `, } ); -export const RULES_BULK_EDIT_SUCCESS_INDEX_EDIT_DESCRIPTION = i18n.translate( +export const RULES_BULK_EDIT_SUCCESS_DATA_VIEW_RULES_SKIPPED_DETAIL = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.successIndexEditToastDescription', { defaultMessage: @@ -1090,12 +1094,16 @@ export const RULES_BULK_EDIT_FAILURE = i18n.translate( } ); -export const RULES_BULK_EDIT_FAILURE_DESCRIPTION = (rulesCount: number) => +export const RULES_BULK_EDIT_FAILURE_DESCRIPTION = ( + failedRulesCount: number, + skippedRulesCount: number +) => i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.errorToastDescription', { - values: { rulesCount }, - defaultMessage: '{rulesCount, plural, =1 {# rule} other {# rules}} failed to update.', + values: { failedRulesCount, skippedRulesCount }, + defaultMessage: + '{failedRulesCount, plural, =0 {} =1 {# rule} other {# rules}} failed to update. {skippedRulesCount, plural, =0 {} =1 { # rule was skipped.} other { # rules were skipped.}}', } ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/test_adapters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/test_adapters.ts index 9da1573738d82..ed15fd08c9428 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/test_adapters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/test_adapters.ts @@ -33,7 +33,10 @@ const buildResponses = (method: Method, calls: MockCall[]): ResponseCall[] => { case 'custom': return calls.map(([call]) => ({ status: call.statusCode, - body: JSON.parse(call.body), + body: + Buffer.isBuffer(call.body) || typeof call.body === 'string' + ? JSON.parse(call.body) + : call.body, })); case 'customError': return calls.map(([call]) => ({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts index f64de099dfcbe..a7034123e7a96 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts @@ -75,6 +75,7 @@ describe('Perform bulk action route', () => { results: someBulkActionResults(), summary: { failed: 0, + skipped: 0, succeeded: 1, total: 1, }, @@ -96,6 +97,7 @@ describe('Perform bulk action route', () => { results: someBulkActionResults(), summary: { failed: 0, + skipped: 0, succeeded: 0, total: 0, }, @@ -148,6 +150,7 @@ describe('Perform bulk action route', () => { results: someBulkActionResults(), summary: { failed: 1, + skipped: 0, succeeded: 0, total: 1, }, @@ -185,6 +188,7 @@ describe('Perform bulk action route', () => { results: someBulkActionResults(), summary: { failed: 1, + skipped: 0, succeeded: 0, total: 1, }, @@ -224,6 +228,7 @@ describe('Perform bulk action route', () => { results: someBulkActionResults(), summary: { failed: 1, + skipped: 0, succeeded: 0, total: 1, }, @@ -236,6 +241,7 @@ describe('Perform bulk action route', () => { it('returns partial failure error if update of few rules fail', async () => { clients.rulesClient.bulkEdit.mockResolvedValue({ rules: [mockRule, mockRule], + skipped: [], errors: [ { message: 'mocked validation message', @@ -264,6 +270,7 @@ describe('Perform bulk action route', () => { summary: { failed: 3, succeeded: 2, + skipped: 0, total: 5, }, errors: [ @@ -333,6 +340,7 @@ describe('Perform bulk action route', () => { attributes: { summary: { failed: 1, + skipped: 0, succeeded: 1, total: 2, }, @@ -355,6 +363,133 @@ describe('Perform bulk action route', () => { }); }); + describe('rule skipping', () => { + it('returns partial failure error with skipped rules if some rule updates fail and others are skipped', async () => { + clients.rulesClient.bulkEdit.mockResolvedValue({ + rules: [mockRule, mockRule], + skipped: [ + { id: 'skipped-rule-id-1', name: 'Skipped Rule 1', skip_reason: 'RULE_NOT_MODIFIED' }, + { id: 'skipped-rule-id-2', name: 'Skipped Rule 2', skip_reason: 'RULE_NOT_MODIFIED' }, + ], + errors: [ + { + message: 'test failure', + rule: { id: 'failed-rule-id-3', name: 'Detect Root/Admin Users' }, + }, + ], + total: 5, + }); + + const response = await server.inject( + getBulkActionEditRequest(), + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + attributes: { + summary: { + failed: 1, + skipped: 2, + succeeded: 2, + total: 5, + }, + errors: [ + { + message: 'test failure', + rules: [ + { + id: 'failed-rule-id-3', + name: 'Detect Root/Admin Users', + }, + ], + status_code: 500, + }, + ], + results: someBulkActionResults(), + }, + message: 'Bulk edit partially failed', + status_code: 500, + }); + }); + + it('returns success with skipped rules if some rules are skipped, but no errors are reported', async () => { + clients.rulesClient.bulkEdit.mockResolvedValue({ + rules: [mockRule, mockRule], + skipped: [ + { id: 'skipped-rule-id-1', name: 'Skipped Rule 1', skip_reason: 'RULE_NOT_MODIFIED' }, + { id: 'skipped-rule-id-2', name: 'Skipped Rule 2', skip_reason: 'RULE_NOT_MODIFIED' }, + ], + errors: [], + total: 4, + }); + + const response = await server.inject( + getBulkActionEditRequest(), + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + attributes: { + summary: { + failed: 0, + skipped: 2, + succeeded: 2, + total: 4, + }, + results: someBulkActionResults(), + }, + rules_count: 4, + success: true, + }); + }); + + it('returns 500 with skipped rules if some rules are skipped, but some errors are reported', async () => { + clients.rulesClient.bulkEdit.mockResolvedValue({ + rules: [mockRule, mockRule], + skipped: [ + { id: 'skipped-rule-id-1', name: 'Skipped Rule 1', skip_reason: 'RULE_NOT_MODIFIED' }, + { id: 'skipped-rule-id-2', name: 'Skipped Rule 2', skip_reason: 'RULE_NOT_MODIFIED' }, + ], + errors: [ + { + message: 'test failure', + rule: { id: 'failed-rule-id-3', name: 'Detect Root/Admin Users' }, + }, + ], + total: 5, + }); + + const response = await server.inject( + getBulkActionEditRequest(), + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + attributes: { + summary: { + failed: 1, + skipped: 2, + succeeded: 2, + total: 5, + }, + results: someBulkActionResults(), + errors: [ + { + message: 'test failure', + rules: [{ id: 'failed-rule-id-3', name: 'Detect Root/Admin Users' }], + status_code: 500, + }, + ], + }, + message: 'Bulk edit partially failed', + status_code: 500, + }); + }); + }); + describe('request validation', () => { it('rejects payloads with no action', async () => { const request = requestMock.create({ @@ -504,7 +639,7 @@ describe('Perform bulk action route', () => { success: true, rules_count: rulesNumber, attributes: { - summary: { failed: 0, succeeded: rulesNumber, total: rulesNumber }, + summary: { failed: 0, skipped: 0, succeeded: rulesNumber, total: rulesNumber }, results: someBulkActionResults(), }, }) @@ -517,5 +652,6 @@ function someBulkActionResults() { created: expect.any(Array), deleted: expect.any(Array), updated: expect.any(Array), + skipped: expect.any(Array), }; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts index 3e884113f2c84..82660cba2a870 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts @@ -8,13 +8,17 @@ import { truncate } from 'lodash'; import moment from 'moment'; import { BadRequestError, transformError } from '@kbn/securitysolution-es-utils'; -import type { KibanaResponseFactory, Logger, SavedObjectsClientContract } from '@kbn/core/server'; +import type { + IKibanaResponse, + KibanaResponseFactory, + Logger, + SavedObjectsClientContract, +} from '@kbn/core/server'; import type { RulesClient, BulkOperationError } from '@kbn/alerting-plugin/server'; -import type { SanitizedRule } from '@kbn/alerting-plugin/common'; +import type { SanitizedRule, BulkActionSkipResult } from '@kbn/alerting-plugin/common'; import { AbortError } from '@kbn/kibana-utils-plugin/common'; import type { RuleAlertType, RuleParams } from '../../../../rule_schema'; - import type { BulkActionsDryRunErrCode } from '../../../../../../../common/constants'; import { DETECTION_ENGINE_RULES_BULK_ACTION, @@ -26,6 +30,13 @@ import { PerformBulkActionRequestBody, PerformBulkActionRequestQuery, } from '../../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; +import type { + NormalizedRuleError, + RuleDetailsInError, + BulkEditActionResponse, + BulkEditActionResults, + BulkEditActionSummary, +} from '../../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/response_schema'; import type { SetupPlugins } from '../../../../../../plugin'; import type { SecuritySolutionPluginRouter } from '../../../../../../types'; import { buildRouteValidation } from '../../../../../../utils/build_validation/route_validation'; @@ -56,18 +67,7 @@ const MAX_RULES_TO_PROCESS_TOTAL = 10000; const MAX_ERROR_MESSAGE_LENGTH = 1000; const MAX_ROUTE_CONCURRENCY = 5; -interface RuleDetailsInError { - id: string; - name?: string; -} -interface NormalizedRuleError { - message: string; - status_code: number; - err_code?: BulkActionsDryRunErrCode; - rules: RuleDetailsInError[]; -} - -type BulkActionError = +export type BulkActionError = | PromisePoolError | PromisePoolError | BulkOperationError; @@ -124,61 +124,66 @@ const buildBulkResponse = ( updated = [], created = [], deleted = [], + skipped = [], }: { isDryRun?: boolean; errors?: BulkActionError[]; updated?: RuleAlertType[]; created?: RuleAlertType[]; deleted?: RuleAlertType[]; + skipped?: BulkActionSkipResult[]; } -) => { +): IKibanaResponse => { const numSucceeded = updated.length + created.length + deleted.length; + const numSkipped = skipped.length; const numFailed = errors.length; - const summary = { + + const summary: BulkEditActionSummary = { failed: numFailed, succeeded: numSucceeded, - total: numSucceeded + numFailed, + skipped: numSkipped, + total: numSucceeded + numFailed + numSkipped, }; // if response is for dry_run, empty lists of rules returned, as rules are not actually updated and stored within ES // thus, it's impossible to return reliably updated/duplicated/deleted rules - const results = isDryRun + const results: BulkEditActionResults = isDryRun ? { updated: [], created: [], deleted: [], + skipped: [], } : { updated: updated.map((rule) => internalRuleToAPIResponse(rule)), created: created.map((rule) => internalRuleToAPIResponse(rule)), deleted: deleted.map((rule) => internalRuleToAPIResponse(rule)), + skipped, }; if (numFailed > 0) { - return response.custom({ + return response.custom({ headers: { 'content-type': 'application/json' }, - body: Buffer.from( - JSON.stringify({ - message: summary.succeeded > 0 ? 'Bulk edit partially failed' : 'Bulk edit failed', - status_code: 500, - attributes: { - errors: normalizeErrorResponse(errors), - results, - summary, - }, - }) - ), + body: { + message: summary.succeeded > 0 ? 'Bulk edit partially failed' : 'Bulk edit failed', + status_code: 500, + attributes: { + errors: normalizeErrorResponse(errors), + results, + summary, + }, + }, statusCode: 500, }); } - return response.ok({ - body: { - success: true, - rules_count: summary.total, - attributes: { results, summary }, - }, - }); + const responseBody: BulkEditActionResponse = { + success: true, + rules_count: summary.total, + attributes: { results, summary }, + }; + + return response.ok({ body: responseBody }); }; const fetchRulesByQueryOrIds = async ({ @@ -340,7 +345,7 @@ export const performBulkActionRoute = ( // handling this action before switch statement as bulkEditRules fetch rules within // rulesClient method, hence there is no need to use fetchRulesByQueryOrIds utility if (body.action === BulkActionType.edit && !isDryRun) { - const { rules, errors } = await bulkEditRules({ + const { rules, errors, skipped } = await bulkEditRules({ rulesClient, filter: query, ids: body.ids, @@ -371,6 +376,7 @@ export const performBulkActionRoute = ( updated: migrationOutcome.results .filter(({ result }) => result) .map(({ result }) => result), + skipped, errors: [...errors, ...migrationOutcome.errors], }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts index 89abe357bb58b..6660ce730f84b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts @@ -7,7 +7,6 @@ import type { BulkOperationError, RulesClient } from '@kbn/alerting-plugin/server'; import pMap from 'p-map'; - import { MAX_RULES_TO_UPDATE_IN_PARALLEL, NOTIFICATION_THROTTLE_NO_ACTIONS, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts index ee4b19f12d81b..6d65665713ade 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts @@ -45,23 +45,23 @@ describe('ruleParamsModifier', () => { } as RuleAlertType['params']; test('should increment version if rule is custom (immutable === false)', () => { - const editedRuleParams = ruleParamsModifier(ruleParamsMock, [ + const { modifiedParams } = ruleParamsModifier(ruleParamsMock, [ { type: BulkActionEditType.add_index_patterns, value: ['my-index-*'], }, ]); - expect(editedRuleParams).toHaveProperty('version', ruleParamsMock.version + 1); + expect(modifiedParams).toHaveProperty('version', ruleParamsMock.version + 1); }); test('should not increment version if rule is prebuilt (immutable === true)', () => { - const editedRuleParams = ruleParamsModifier({ ...ruleParamsMock, immutable: true }, [ + const { modifiedParams } = ruleParamsModifier({ ...ruleParamsMock, immutable: true }, [ { type: BulkActionEditType.add_index_patterns, value: ['my-index-*'], }, ]); - expect(editedRuleParams).toHaveProperty('version', ruleParamsMock.version); + expect(modifiedParams).toHaveProperty('version', ruleParamsMock.version); }); describe('index_patterns', () => { @@ -73,6 +73,7 @@ describe('ruleParamsModifier', () => { existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'], indexPatternsToAdd: ['index-2-*', 'index-3-*'], resultingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'], + isParamsUpdateSkipped: true, }, ], [ @@ -87,6 +88,7 @@ describe('ruleParamsModifier', () => { 'index-4-*', 'index-5-*', ], + isParamsUpdateSkipped: false, }, ], [ @@ -101,6 +103,7 @@ describe('ruleParamsModifier', () => { 'index-4-*', 'index-5-*', ], + isParamsUpdateSkipped: false, }, ], [ @@ -109,12 +112,21 @@ describe('ruleParamsModifier', () => { existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'], indexPatternsToAdd: [], resultingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'], + isParamsUpdateSkipped: true, }, ], ])( 'should add index patterns to rule, case:"%s"', - (caseName, { existingIndexPatterns, indexPatternsToAdd, resultingIndexPatterns }) => { - const editedRuleParams = ruleParamsModifier( + ( + caseName, + { + existingIndexPatterns, + indexPatternsToAdd, + resultingIndexPatterns, + isParamsUpdateSkipped, + } + ) => { + const { modifiedParams, isParamsUpdateSkipped: isUpdateSkipped } = ruleParamsModifier( { ...ruleParamsMock, index: existingIndexPatterns } as RuleAlertType['params'], [ { @@ -123,7 +135,8 @@ describe('ruleParamsModifier', () => { }, ] ); - expect(editedRuleParams).toHaveProperty('index', resultingIndexPatterns); + expect(modifiedParams).toHaveProperty('index', resultingIndexPatterns); + expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); } ); }); @@ -136,6 +149,7 @@ describe('ruleParamsModifier', () => { existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'], indexPatternsToDelete: ['index-2-*', 'index-3-*'], resultingIndexPatterns: ['index-1-*'], + isParamsUpdateSkipped: false, }, ], [ @@ -144,6 +158,7 @@ describe('ruleParamsModifier', () => { existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'], indexPatternsToDelete: ['index-4-*', 'index-5-*'], resultingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'], + isParamsUpdateSkipped: true, }, ], [ @@ -152,6 +167,7 @@ describe('ruleParamsModifier', () => { existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'], indexPatternsToDelete: ['index-3-*', 'index-4-*', 'index-5-*'], resultingIndexPatterns: ['index-1-*', 'index-2-*'], + isParamsUpdateSkipped: false, }, ], [ @@ -160,12 +176,21 @@ describe('ruleParamsModifier', () => { existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'], indexPatternsToDelete: [], resultingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'], + isParamsUpdateSkipped: true, }, ], ])( 'should delete index patterns from rule, case:"%s"', - (caseName, { existingIndexPatterns, indexPatternsToDelete, resultingIndexPatterns }) => { - const editedRuleParams = ruleParamsModifier( + ( + caseName, + { + existingIndexPatterns, + indexPatternsToDelete, + resultingIndexPatterns, + isParamsUpdateSkipped, + } + ) => { + const { modifiedParams, isParamsUpdateSkipped: isUpdateSkipped } = ruleParamsModifier( { ...ruleParamsMock, index: existingIndexPatterns } as RuleAlertType['params'], [ { @@ -174,7 +199,8 @@ describe('ruleParamsModifier', () => { }, ] ); - expect(editedRuleParams).toHaveProperty('index', resultingIndexPatterns); + expect(modifiedParams).toHaveProperty('index', resultingIndexPatterns); + expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); } ); }); @@ -187,6 +213,7 @@ describe('ruleParamsModifier', () => { existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'], indexPatternsToOverwrite: ['index-2-*', 'index-3-*'], resultingIndexPatterns: ['index-2-*', 'index-3-*'], + isParamsUpdateSkipped: false, }, ], [ @@ -195,6 +222,7 @@ describe('ruleParamsModifier', () => { existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'], indexPatternsToOverwrite: ['index-4-*', 'index-5-*'], resultingIndexPatterns: ['index-4-*', 'index-5-*'], + isParamsUpdateSkipped: false, }, ], [ @@ -203,12 +231,21 @@ describe('ruleParamsModifier', () => { existingIndexPatterns: ['index-1-*', 'index-2-*', 'index-3-*'], indexPatternsToOverwrite: ['index-3-*', 'index-4-*', 'index-5-*'], resultingIndexPatterns: ['index-3-*', 'index-4-*', 'index-5-*'], + isParamsUpdateSkipped: false, }, ], ])( 'should overwrite index patterns in rule, case:"%s"', - (caseName, { existingIndexPatterns, indexPatternsToOverwrite, resultingIndexPatterns }) => { - const editedRuleParams = ruleParamsModifier( + ( + caseName, + { + existingIndexPatterns, + indexPatternsToOverwrite, + resultingIndexPatterns, + isParamsUpdateSkipped, + } + ) => { + const { modifiedParams, isParamsUpdateSkipped: isUpdateSkipped } = ruleParamsModifier( { ...ruleParamsMock, index: existingIndexPatterns } as RuleAlertType['params'], [ { @@ -217,14 +254,16 @@ describe('ruleParamsModifier', () => { }, ] ); - expect(editedRuleParams).toHaveProperty('index', resultingIndexPatterns); + expect(modifiedParams).toHaveProperty('index', resultingIndexPatterns); + expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); } ); }); test('should return undefined index patterns on remove action if rule has dataViewId only', () => { const testDataViewId = 'test-data-view-id'; - const editedRuleParams = ruleParamsModifier( + + const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier( { dataViewId: testDataViewId } as RuleAlertType['params'], [ { @@ -233,12 +272,12 @@ describe('ruleParamsModifier', () => { }, ] ); - expect(editedRuleParams).not.toHaveProperty('index'); - expect(editedRuleParams).toHaveProperty('dataViewId', testDataViewId); + expect(modifiedParams).not.toHaveProperty('index'); + expect(isParamsUpdateSkipped).toBe(true); }); test('should set dataViewId to undefined if overwrite_data_views=true on set_index_patterns action', () => { - const editedRuleParams = ruleParamsModifier( + const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier( { dataViewId: 'test-data-view', index: ['test-*'] } as RuleAlertType['params'], [ { @@ -248,11 +287,12 @@ describe('ruleParamsModifier', () => { }, ] ); - expect(editedRuleParams).toHaveProperty('dataViewId', undefined); + expect(modifiedParams).toHaveProperty('dataViewId', undefined); + expect(isParamsUpdateSkipped).toBe(false); }); test('should set dataViewId to undefined if overwrite_data_views=true on add_index_patterns action', () => { - const editedRuleParams = ruleParamsModifier( + const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier( { dataViewId: 'test-data-view', index: ['test-*'] } as RuleAlertType['params'], [ { @@ -262,11 +302,12 @@ describe('ruleParamsModifier', () => { }, ] ); - expect(editedRuleParams).toHaveProperty('dataViewId', undefined); + expect(modifiedParams).toHaveProperty('dataViewId', undefined); + expect(isParamsUpdateSkipped).toBe(false); }); test('should set dataViewId to undefined if overwrite_data_views=true on delete_index_patterns action', () => { - const editedRuleParams = ruleParamsModifier( + const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier( { dataViewId: 'test-data-view', index: ['test-*', 'index'] } as RuleAlertType['params'], [ { @@ -276,12 +317,13 @@ describe('ruleParamsModifier', () => { }, ] ); - expect(editedRuleParams).toHaveProperty('dataViewId', undefined); - expect(editedRuleParams).toHaveProperty('index', ['test-*']); + expect(modifiedParams).toHaveProperty('dataViewId', undefined); + expect(modifiedParams).toHaveProperty('index', ['test-*']); + expect(isParamsUpdateSkipped).toBe(false); }); test('should set dataViewId to undefined and index to undefined if overwrite_data_views=true on delete_index_patterns action and rule had no index patterns to begin with', () => { - const editedRuleParams = ruleParamsModifier( + const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier( { dataViewId: 'test-data-view', index: undefined } as RuleAlertType['params'], [ { @@ -291,8 +333,9 @@ describe('ruleParamsModifier', () => { }, ] ); - expect(editedRuleParams).toHaveProperty('dataViewId', undefined); - expect(editedRuleParams).toHaveProperty('index', undefined); + expect(modifiedParams).toHaveProperty('dataViewId', undefined); + expect(modifiedParams).toHaveProperty('index', undefined); + expect(isParamsUpdateSkipped).toBe(false); }); test('should throw error on adding index pattern if rule is of machine learning type', () => { @@ -337,7 +380,7 @@ describe('ruleParamsModifier', () => { describe('timeline', () => { test('should set timeline', () => { - const editedRuleParams = ruleParamsModifier(ruleParamsMock, [ + const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier(ruleParamsMock, [ { type: BulkActionEditType.set_timeline, value: { @@ -347,8 +390,9 @@ describe('ruleParamsModifier', () => { }, ]); - expect(editedRuleParams.timelineId).toBe('91832785-286d-4ebe-b884-1a208d111a70'); - expect(editedRuleParams.timelineTitle).toBe('Test timeline'); + expect(modifiedParams.timelineId).toBe('91832785-286d-4ebe-b884-1a208d111a70'); + expect(modifiedParams.timelineTitle).toBe('Test timeline'); + expect(isParamsUpdateSkipped).toBe(false); }); }); @@ -357,7 +401,7 @@ describe('ruleParamsModifier', () => { const INTERVAL_IN_MINUTES = 5; const LOOKBACK_IN_MINUTES = 1; const FROM_IN_SECONDS = (INTERVAL_IN_MINUTES + LOOKBACK_IN_MINUTES) * 60; - const editedRuleParams = ruleParamsModifier(ruleParamsMock, [ + const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier(ruleParamsMock, [ { type: BulkActionEditType.set_schedule, value: { @@ -368,11 +412,12 @@ describe('ruleParamsModifier', () => { ]); // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((editedRuleParams as any).interval).toBeUndefined(); - expect(editedRuleParams.meta).toStrictEqual({ + expect((modifiedParams as any).interval).toBeUndefined(); + expect(modifiedParams.meta).toStrictEqual({ from: '1m', }); - expect(editedRuleParams.from).toBe(`now-${FROM_IN_SECONDS}s`); + expect(modifiedParams.from).toBe(`now-${FROM_IN_SECONDS}s`); + expect(isParamsUpdateSkipped).toBe(false); }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts index 9f5f252b9fd86..e92b6e6ab0163 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts @@ -5,12 +5,14 @@ * 2.0. */ -/* eslint-disable complexity */ - import moment from 'moment'; import { parseInterval } from '@kbn/data-plugin/common/search/aggs/utils/date_interval_utils'; +import type { RuleParamsModifierResult } from '@kbn/alerting-plugin/server/rules_client/methods/bulk_edit'; import type { RuleAlertType } from '../../../rule_schema'; -import type { BulkActionEditForRuleParams } from '../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; +import type { + BulkActionEditForRuleParams, + BulkActionEditPayloadIndexPatterns, +} from '../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; import { BulkActionEditType } from '../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; import { invariant } from '../../../../../../common/utils/invariant'; @@ -22,22 +24,70 @@ export const deleteItemsFromArray = (arr: T[], items: T[]): T[] => { return arr.filter((item) => !itemsSet.has(item)); }; +// Check if current params have a configured data view id +// and the action is not set to overwrite data views +const isDataViewExistsAndNotOverriden = ( + dataViewId: string | undefined, + action: BulkActionEditPayloadIndexPatterns +) => dataViewId != null && !action.overwrite_data_views; + +// Check if the index patterns added to the rule already exist in it +const hasIndexPatterns = ( + indexPatterns: string[] | undefined, + action: BulkActionEditPayloadIndexPatterns +) => action.value.every((indexPattern) => indexPatterns?.includes(indexPattern)); + +// Check if the index patterns to be deleted don't exist in the rule +const hasNotIndexPattern = ( + indexPatterns: string[] | undefined, + action: BulkActionEditPayloadIndexPatterns +) => action.value.every((indexPattern) => !indexPatterns?.includes(indexPattern)); + +const shouldSkipIndexPatternsBulkAction = ( + indexPatterns: string[] | undefined, + dataViewId: string | undefined, + action: BulkActionEditPayloadIndexPatterns +) => { + if (isDataViewExistsAndNotOverriden(dataViewId, action)) { + return true; + } + + if (action.type === BulkActionEditType.add_index_patterns) { + return hasIndexPatterns(indexPatterns, action); + } + + if (action.type === BulkActionEditType.delete_index_patterns) { + return hasNotIndexPattern(indexPatterns, action); + } + + return false; +}; + const applyBulkActionEditToRuleParams = ( existingRuleParams: RuleAlertType['params'], action: BulkActionEditForRuleParams -): RuleAlertType['params'] => { +): { + ruleParams: RuleAlertType['params']; + isActionSkipped: boolean; +} => { let ruleParams = { ...existingRuleParams }; + // If the action is succesfully applied and the rule params are modified, + // we update the following flag to false. As soon as the current function + // returns this flag as false, at least once, for any action, we know that + // the rule needs to be marked as having its params updated. + let isActionSkipped = false; switch (action.type) { // index_patterns actions // index pattern is not present in machine learning rule type, so we throw error on it - case BulkActionEditType.add_index_patterns: + case BulkActionEditType.add_index_patterns: { invariant( ruleParams.type !== 'machine_learning', "Index patterns can't be added. Machine learning rule doesn't have index patterns property" ); - if (ruleParams.dataViewId != null && !action.overwrite_data_views) { + if (shouldSkipIndexPatternsBulkAction(ruleParams.index, ruleParams.dataViewId, action)) { + isActionSkipped = true; break; } @@ -47,14 +97,18 @@ const applyBulkActionEditToRuleParams = ( ruleParams.index = addItemsToArray(ruleParams.index ?? [], action.value); break; - - case BulkActionEditType.delete_index_patterns: + } + case BulkActionEditType.delete_index_patterns: { invariant( ruleParams.type !== 'machine_learning', "Index patterns can't be deleted. Machine learning rule doesn't have index patterns property" ); - if (ruleParams.dataViewId != null && !action.overwrite_data_views) { + if ( + !action.overwrite_data_views && + shouldSkipIndexPatternsBulkAction(ruleParams.index, ruleParams.dataViewId, action) + ) { + isActionSkipped = true; break; } @@ -66,14 +120,15 @@ const applyBulkActionEditToRuleParams = ( ruleParams.index = deleteItemsFromArray(ruleParams.index, action.value); } break; - - case BulkActionEditType.set_index_patterns: + } + case BulkActionEditType.set_index_patterns: { invariant( ruleParams.type !== 'machine_learning', "Index patterns can't be overwritten. Machine learning rule doesn't have index patterns property" ); - if (ruleParams.dataViewId != null && !action.overwrite_data_views) { + if (shouldSkipIndexPatternsBulkAction(ruleParams.index, ruleParams.dataViewId, action)) { + isActionSkipped = true; break; } @@ -83,16 +138,17 @@ const applyBulkActionEditToRuleParams = ( ruleParams.index = action.value; break; - + } // timeline actions - case BulkActionEditType.set_timeline: + case BulkActionEditType.set_timeline: { ruleParams = { ...ruleParams, timelineId: action.value.timeline_id || undefined, timelineTitle: action.value.timeline_title || undefined, }; - break; + break; + } // update look-back period in from and meta.from fields case BulkActionEditType.set_schedule: { const interval = parseInterval(action.value.interval) ?? moment.duration(0); @@ -108,26 +164,35 @@ const applyBulkActionEditToRuleParams = ( }, from: `now-${from}s`, }; + + break; } } - return ruleParams; + return { ruleParams, isActionSkipped }; }; /** * takes list of bulkEdit actions and apply them to rule.params by mutating it * @param existingRuleParams * @param actions - * @returns mutated params + * @returns mutated params, isParamsUpdateSkipped flag */ export const ruleParamsModifier = ( existingRuleParams: RuleAlertType['params'], actions: BulkActionEditForRuleParams[] -) => { - const modifiedParams = actions.reduce( - (acc, action) => ({ ...acc, ...applyBulkActionEditToRuleParams(acc, action) }), - existingRuleParams - ); +): RuleParamsModifierResult => { + let isParamsUpdateSkipped = true; + + const modifiedParams = actions.reduce((acc, action) => { + const { ruleParams, isActionSkipped } = applyBulkActionEditToRuleParams(acc, action); + + // The rule was updated with at least one action, so mark our rule as updated + if (!isActionSkipped) { + isParamsUpdateSkipped = false; + } + return { ...acc, ...ruleParams }; + }, existingRuleParams); // increment version even if actions are empty, as attributes can be modified as well outside of ruleParamsModifier // version must not be modified for immutable rule. Otherwise prebuilt rules upgrade flow will be broken @@ -135,5 +200,5 @@ export const ruleParamsModifier = ( modifiedParams.version += 1; } - return modifiedParams; + return { modifiedParams, isParamsUpdateSkipped }; }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f9c4359d9285f..c6c3752bfc724 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -26923,9 +26923,7 @@ "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.infoCalloutTitle": "Configurer les actions pour {rulesCount, plural, one {# règle que vous avez sélectionnée} other {# règles que vous avez sélectionnées}}", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.warningCalloutMessage": "Vous êtes sur le point d'écraser les actions de règle pour {rulesCount, plural, one {# règle sélectionnée} other {# règles sélectionnées}}. Cliquez sur {saveButton} pour appliquer les modifications.", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.warningCalloutMessage": "Vous êtes sur le point d'appliquer des modifications à {rulesCount, plural, one {# règle sélectionnée} other {# règles sélectionnées}}. Si vous avez déjà appliqué des modèles de chronologie à ces règles, ils seront remplacés ou (si vous sélectionnez \"Aucun\") réinitialisés.", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.errorToastDescription": "Impossible de mettre à jour {rulesCount, plural, =1 {# règle} other {# règles}}.", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.setSchedule.warningCalloutMessage": "Vous êtes sur le point d'appliquer des modifications à {rulesCount, plural, one {# règle sélectionnée} other {# règles sélectionnées}}. Les modifications que vous effectuez écraseront les planifications existantes de la règle et le temps de récupération supplémentaire (le cas échéant).", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.successToastDescription": "Vous avez correctement mis à jour {rulesCount, plural, =1 {# règle} other {# règles}}", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.elasticRulesEditDescription": "{rulesCount, plural, =1 {# règle Elastic prédéfinie} other {# règles Elastic prédéfinies}} (modification des règles prédéfinies non prise en charge)", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.elasticRulesExportDescription": "{rulesCount, plural, =1 {# règle Elastic prédéfinie} other {# règles Elastic prédéfinies}} (exportation des règles prédéfinies non prise en charge)", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.enable.errorToastDescription": "Impossible d'activer {rulesCount, plural, =1 {# règle} other {# règles}}.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2925afe9e3a1f..95aef8c8ef76a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -26898,9 +26898,7 @@ "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.infoCalloutTitle": "選択した{rulesCount, plural, other {# ルール}}のアクションを設定", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.warningCalloutMessage": "{rulesCount, plural, other {# 個の選択したルール}}のルールを上書きしようとしています。{saveButton}をクリックすると、変更が適用されます。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.warningCalloutMessage": "{rulesCount, plural, other {# 個の選択したルール}}に変更を適用しようとしています。以前にタイムラインテンプレートをこれらのルールに適用している場合は、上書きされるか、([なし]を選択した場合は)[なし]にリセットされます。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.errorToastDescription": "{rulesCount, plural, other {#個のルール}}を更新できませんでした。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.setSchedule.warningCalloutMessage": "{rulesCount, plural, other {# 個の選択したルール}}に変更を適用しようとしています。行った変更は、既存のルールスケジュールと追加の振り返り時間(該当する場合)を上書きします。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.successToastDescription": "{rulesCount, plural, other {#個のルール}}を正常に更新しました", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.elasticRulesEditDescription": "{rulesCount, plural, other {# 個の既製のElasticルール}}(既製のルールは編集できません)", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.elasticRulesExportDescription": "{rulesCount, plural, other {# 個の既製のElasticルール}}(既製のルールはエクスポートできません)", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.enable.errorToastDescription": "{rulesCount, plural, other {#個のルール}}を有効にできませんでした。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index aac0c566a6732..8acf63ed7d3e8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -26931,9 +26931,7 @@ "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.infoCalloutTitle": "为您选择的 {rulesCount, plural, other {# 个规则}}配置操作", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.addRuleActions.warningCalloutMessage": "您即将覆盖 {rulesCount, plural, other {# 个选定规则}}的规则操作。单击 {saveButton} 以应用更改。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.applyTimelineTemplate.warningCalloutMessage": "您即将对 {rulesCount, plural, other {# 个选定规则}}应用更改。如果之前已将时间线模板应用于这些规则,则会将其覆盖或重置为无(如果您选择了“无”)。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.errorToastDescription": "无法更新 {rulesCount, plural, other {# 个规则}}。", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.setSchedule.warningCalloutMessage": "您即将对 {rulesCount, plural, other {# 个选定规则}}应用更改。您所做的更改将覆盖现有规则计划和其他回查时间(如有)。", - "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.edit.successToastDescription": "您已成功更新 {rulesCount, plural, other {# 个规则}}", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.elasticRulesEditDescription": "{rulesCount, plural, other {# 个预构建的 Elastic 规则}}(不支持编辑预构建的规则)", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.elasticRulesExportDescription": "{rulesCount, plural, other {# 个预构建的 Elastic 规则}}(不支持导出预构建的规则)", "xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.enable.errorToastDescription": "无法启用 {rulesCount, plural, other {# 个规则}}。", diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts index 53ee50a27012c..df53acd6acdbd 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts @@ -102,6 +102,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { }, ], rules: [], + skipped: [], total: 1, }); expect(response.statusCode).to.eql(200); @@ -241,7 +242,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.body).to.eql({ errors: [], rules: [], total: 0 }); + expect(response.body).to.eql({ errors: [], rules: [], skipped: [], total: 0 }); expect(response.statusCode).to.eql(200); break; case 'global_read at space1': @@ -382,7 +383,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { break; case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': - expect(response.body).to.eql({ errors: [], rules: [], total: 0 }); + expect(response.body).to.eql({ errors: [], rules: [], skipped: [], total: 0 }); expect(response.statusCode).to.eql(200); break; case 'global_read at space1': @@ -587,7 +588,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'superuser at space1': case 'global_read at space1': - expect(response.body).to.eql({ rules: [], errors: [], total: 0 }); + expect(response.body).to.eql({ rules: [], skipped: [], errors: [], total: 0 }); expect(response.statusCode).to.eql(200); break; case 'no_kibana_privileges at space1': diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts index 48117cc08ecf6..06e55fdfdda1b 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts @@ -118,7 +118,7 @@ export default ({ getService }: FtrProviderContext): void => { .send({ query: '', action: BulkActionType.delete }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the deleted rule is returned with the response expect(body.attributes.results.deleted[0].name).to.eql(testRule.name); @@ -153,7 +153,7 @@ export default ({ getService }: FtrProviderContext): void => { .send({ query: '', action: BulkActionType.delete }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the deleted rule is returned with the response expect(body.attributes.results.deleted[0].name).to.eql(rule1.name); @@ -174,7 +174,7 @@ export default ({ getService }: FtrProviderContext): void => { .send({ query: '', action: BulkActionType.enable }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response expect(body.attributes.results.updated[0].enabled).to.eql(true); @@ -210,7 +210,7 @@ export default ({ getService }: FtrProviderContext): void => { .send({ query: '', action: BulkActionType.enable }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response expect(body.attributes.results.updated[0].enabled).to.eql(true); @@ -243,7 +243,7 @@ export default ({ getService }: FtrProviderContext): void => { .send({ query: '', action: BulkActionType.disable }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response expect(body.attributes.results.updated[0].enabled).to.eql(false); @@ -279,7 +279,7 @@ export default ({ getService }: FtrProviderContext): void => { .send({ query: '', action: BulkActionType.disable }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response expect(body.attributes.results.updated[0].enabled).to.eql(false); @@ -317,7 +317,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the duplicated rule is returned with the response expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`); @@ -363,7 +363,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the duplicated rule is returned with the response expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`); @@ -452,6 +452,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(bulkEditResponse.attributes.summary).to.eql({ failed: 0, + skipped: 0, succeeded: 1, total: 1, }); @@ -473,30 +474,12 @@ export default ({ getService }: FtrProviderContext): void => { tagsToDelete: ['tag1', 'tag2'], resultingTags: ['tag3'], }, - { - caseName: '3 existing tags - 2 other tags(none of them) = 3 tags', - existingTags: ['tag1', 'tag2', 'tag3'], - tagsToDelete: ['tag4', 'tag5'], - resultingTags: ['tag1', 'tag2', 'tag3'], - }, { caseName: '3 existing tags - 1 of them - 2 other tags(none of them) = 2 tags', existingTags: ['tag1', 'tag2', 'tag3'], tagsToDelete: ['tag3', 'tag4', 'tag5'], resultingTags: ['tag1', 'tag2'], }, - { - caseName: '3 existing tags - 0 tags = 3 tags', - existingTags: ['tag1', 'tag2', 'tag3'], - tagsToDelete: [], - resultingTags: ['tag1', 'tag2', 'tag3'], - }, - { - caseName: '0 existing tags - 2 tags = 0 tags', - existingTags: [], - tagsToDelete: ['tag4', 'tag5'], - resultingTags: [], - }, { caseName: '3 existing tags - 3 of them = 0 tags', existingTags: ['tag1', 'tag2', 'tag3'], @@ -526,6 +509,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(bulkEditResponse.attributes.summary).to.eql({ failed: 0, + skipped: 0, succeeded: 1, total: 1, }); @@ -541,12 +525,6 @@ export default ({ getService }: FtrProviderContext): void => { }); const addTagsCases = [ - { - caseName: '3 existing tags + 2 of them = 3 tags', - existingTags: ['tag1', 'tag2', 'tag3'], - addedTags: ['tag1', 'tag2'], - resultingTags: ['tag1', 'tag2', 'tag3'], - }, { caseName: '3 existing tags + 2 other tags(none of them) = 5 tags', existingTags: ['tag1', 'tag2', 'tag3'], @@ -565,12 +543,6 @@ export default ({ getService }: FtrProviderContext): void => { addedTags: ['tag4', 'tag5'], resultingTags: ['tag4', 'tag5'], }, - { - caseName: '3 existing tags + 0 tags = 3 tags', - existingTags: ['tag1', 'tag2', 'tag3'], - addedTags: [], - resultingTags: ['tag1', 'tag2', 'tag3'], - }, ]; addTagsCases.forEach(({ caseName, existingTags, addedTags, resultingTags }) => { @@ -593,6 +565,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(bulkEditResponse.attributes.summary).to.eql({ failed: 0, + skipped: 0, succeeded: 1, total: 1, }); @@ -606,6 +579,86 @@ export default ({ getService }: FtrProviderContext): void => { expect(updatedRule.tags).to.eql(resultingTags); }); }); + + const skipTagsUpdateCases = [ + // Delete no-ops + { + caseName: '3 existing tags - 0 tags = 3 tags', + existingTags: ['tag1', 'tag2', 'tag3'], + tagsToUpdate: [], + resultingTags: ['tag1', 'tag2', 'tag3'], + operation: BulkActionEditType.delete_tags, + }, + { + caseName: '0 existing tags - 2 tags = 0 tags', + existingTags: [], + tagsToUpdate: ['tag4', 'tag5'], + resultingTags: [], + operation: BulkActionEditType.delete_tags, + }, + { + caseName: '3 existing tags - 2 other tags (none of them) = 3 tags', + existingTags: ['tag1', 'tag2', 'tag3'], + tagsToUpdate: ['tag4', 'tag5'], + resultingTags: ['tag1', 'tag2', 'tag3'], + operation: BulkActionEditType.delete_tags, + }, + // Add no-ops + { + caseName: '3 existing tags + 2 of them = 3 tags', + existingTags: ['tag1', 'tag2', 'tag3'], + tagsToUpdate: ['tag1', 'tag2'], + resultingTags: ['tag1', 'tag2', 'tag3'], + operation: BulkActionEditType.add_tags, + }, + { + caseName: '3 existing tags + 0 tags = 3 tags', + existingTags: ['tag1', 'tag2', 'tag3'], + tagsToUpdate: [], + resultingTags: ['tag1', 'tag2', 'tag3'], + operation: BulkActionEditType.add_tags, + }, + ]; + + skipTagsUpdateCases.forEach( + ({ caseName, existingTags, tagsToUpdate, resultingTags, operation }) => { + it(`should skip rule updated for tags, case: "${caseName}"`, async () => { + const ruleId = 'ruleId'; + + await createRule(supertest, log, { ...getSimpleRule(ruleId), tags: existingTags }); + + const { body: bulkEditResponse } = await postBulkAction() + .send({ + query: '', + action: BulkActionType.edit, + [BulkActionType.edit]: [ + { + type: operation, + value: tagsToUpdate, + }, + ], + }) + .expect(200); + + expect(bulkEditResponse.attributes.summary).to.eql({ + failed: 0, + skipped: 1, + succeeded: 0, + total: 1, + }); + + // Check that the rules is returned as skipped with expected skip reason + expect(bulkEditResponse.attributes.results.skipped[0].skip_reason).to.eql( + 'RULE_NOT_MODIFIED' + ); + + // Check that the no changes have been persisted + const { body: updatedRule } = await fetchRule(ruleId).expect(200); + + expect(updatedRule.tags).to.eql(resultingTags); + }); + } + ); }); describe('index patterns actions', () => { @@ -626,7 +679,12 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(bulkEditResponse.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(bulkEditResponse.attributes.summary).to.eql({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); // Check that the updated rule is returned with the response expect(bulkEditResponse.attributes.results.updated[0].index).to.eql(['initial-index-*']); @@ -656,7 +714,12 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(bulkEditResponse.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(bulkEditResponse.attributes.summary).to.eql({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); // Check that the updated rule is returned with the response expect(bulkEditResponse.attributes.results.updated[0].index).to.eql( @@ -688,7 +751,12 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(bulkEditResponse.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(bulkEditResponse.attributes.summary).to.eql({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); // Check that the updated rule is returned with the response expect(bulkEditResponse.attributes.results.updated[0].index).to.eql( @@ -717,7 +785,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); expect(body.attributes.errors[0]).to.eql({ message: "Index patterns can't be added. Machine learning rule doesn't have index patterns property", @@ -750,7 +818,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); expect(body.attributes.errors[0]).to.eql({ message: "Mutated params invalid: Index patterns can't be empty", status_code: 500, @@ -783,7 +851,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); expect(body.attributes.errors[0]).to.eql({ message: "Mutated params invalid: Index patterns can't be empty", status_code: 500, @@ -800,6 +868,95 @@ export default ({ getService }: FtrProviderContext): void => { expect(reFetchedRule.index).to.eql(['simple-index-*']); }); + + const skipIndexPatternsUpdateCases = [ + // Delete no-ops + { + caseName: '3 existing indeces - 0 indeces = 3 indeces', + existingIndexPatterns: ['index1-*', 'index2-*', 'index3-*'], + indexPatternsToUpdate: [], + resultingIndexPatterns: ['index1-*', 'index2-*', 'index3-*'], + operation: BulkActionEditType.delete_index_patterns, + }, + { + caseName: '0 existing indeces - 2 indeces = 0 indeces', + existingIndexPatterns: [], + indexPatternsToUpdate: ['index1-*', 'index2-*'], + resultingIndexPatterns: [], + operation: BulkActionEditType.delete_index_patterns, + }, + { + caseName: '3 existing indeces - 2 other indeces (none of them) = 3 indeces', + existingIndexPatterns: ['index1-*', 'index2-*', 'index3-*'], + indexPatternsToUpdate: ['index8-*', 'index9-*'], + resultingIndexPatterns: ['index1-*', 'index2-*', 'index3-*'], + operation: BulkActionEditType.delete_index_patterns, + }, + // Add no-ops + { + caseName: '3 existing indeces + 2 exisiting indeces= 3 indeces', + existingIndexPatterns: ['index1-*', 'index2-*', 'index3-*'], + indexPatternsToUpdate: ['index1-*', 'index2-*'], + resultingIndexPatterns: ['index1-*', 'index2-*', 'index3-*'], + operation: BulkActionEditType.add_index_patterns, + }, + { + caseName: '3 existing indeces + 0 indeces = 3 indeces', + existingIndexPatterns: ['index1-*', 'index2-*', 'index3-*'], + indexPatternsToUpdate: [], + resultingIndexPatterns: ['index1-*', 'index2-*', 'index3-*'], + operation: BulkActionEditType.add_index_patterns, + }, + ]; + + skipIndexPatternsUpdateCases.forEach( + ({ + caseName, + existingIndexPatterns, + indexPatternsToUpdate, + resultingIndexPatterns, + operation, + }) => { + it(`should skip rule updated for tags, case: "${caseName}"`, async () => { + const ruleId = 'ruleId'; + + await createRule(supertest, log, { + ...getSimpleRule(ruleId), + index: existingIndexPatterns, + }); + + const { body: bulkEditResponse } = await postBulkAction() + .send({ + query: '', + action: BulkActionType.edit, + [BulkActionType.edit]: [ + { + type: operation, + value: indexPatternsToUpdate, + }, + ], + }) + .expect(200); + + expect(bulkEditResponse.attributes.summary).to.eql({ + failed: 0, + skipped: 1, + succeeded: 0, + total: 1, + }); + + // Check that the rules is returned as skipped with expected skip reason + expect(bulkEditResponse.attributes.results.skipped[0].skip_reason).to.eql( + 'RULE_NOT_MODIFIED' + ); + + // Check that the no changes have been persisted + const { body: updatedRule } = await fetchRule(ruleId).expect(200); + + expect(updatedRule.index).to.eql(resultingIndexPatterns); + }); + } + ); }); it('should migrate legacy actions on edit', async () => { @@ -837,7 +994,12 @@ export default ({ getService }: FtrProviderContext): void => { ], }); - expect(setTagsBody.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(setTagsBody.attributes.summary).to.eql({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); // Check that the updates have been persisted const { body: setTagsRule } = await fetchRule(ruleId).expect(200); @@ -883,7 +1045,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response expect(body.attributes.results.updated[0].timeline_id).to.eql(timelineId); @@ -926,7 +1088,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response expect(body.attributes.results.updated[0].timeline_id).to.be(undefined); @@ -955,7 +1117,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); expect(body.attributes.errors[0]).to.eql({ message: "Index patterns can't be added. Machine learning rule doesn't have index patterns property", @@ -988,7 +1150,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); expect(body.attributes.errors[0]).to.eql({ message: "Mutated params invalid: Index patterns can't be empty", status_code: 500, @@ -1078,7 +1240,12 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 }); + expect(body.attributes.summary).to.eql({ + failed: 1, + skipped: 0, + succeeded: 0, + total: 1, + }); expect(body.attributes.errors[0]).to.eql({ message: "Elastic rule can't be edited", status_code: 500, @@ -1606,7 +1773,12 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 }); + expect(body.attributes.summary).to.eql({ + failed: 1, + skipped: 0, + succeeded: 0, + total: 1, + }); expect(body.attributes.errors[0]).to.eql({ message: "Elastic rule can't be edited", status_code: 500, @@ -1834,7 +2006,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); expect(body.attributes.results.updated[0].interval).to.eql(interval); expect(body.attributes.results.updated[0].meta).to.eql({ from: `${lookbackMinutes}m` }); @@ -1870,7 +2042,12 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(setIndexBody.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(setIndexBody.attributes.summary).to.eql({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); // Check that the updated rule is returned with the response expect(setIndexBody.attributes.results.updated[0].index).to.eql(['initial-index-*']); @@ -1882,15 +2059,15 @@ export default ({ getService }: FtrProviderContext): void => { expect(setIndexRule.index).to.eql(['initial-index-*']); }); - it('should NOT add an index pattern to a rule and overwrite the data view when overwrite_data_views is false', async () => { + it('should return skipped rule and NOT add an index pattern to a rule or overwrite the data view when overwrite_data_views is false', async () => { const ruleId = 'ruleId'; const dataViewId = 'index1-*'; - const simpleRule = { + + const simpleRule = await createRule(supertest, log, { ...getSimpleRule(ruleId), index: undefined, data_view_id: dataViewId, - }; - await createRule(supertest, log, simpleRule); + }); const { body: setIndexBody } = await postBulkAction() .send({ @@ -1906,13 +2083,21 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(setIndexBody.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(setIndexBody.attributes.summary).to.eql({ + failed: 0, + skipped: 1, + succeeded: 0, + total: 1, + }); - // Check that the updated rule is returned with the response - expect(setIndexBody.attributes.results.updated[0].index).to.eql(undefined); - expect(setIndexBody.attributes.results.updated[0].data_view_id).to.eql(dataViewId); + expect(setIndexBody.attributes.errors).to.be(undefined); - // Check that the updates have been persisted + // Check that the skipped rule is returned with the response + expect(setIndexBody.attributes.results.skipped[0].id).to.eql(simpleRule.id); + expect(setIndexBody.attributes.results.skipped[0].name).to.eql(simpleRule.name); + expect(setIndexBody.attributes.results.skipped[0].skip_reason).to.eql('RULE_NOT_MODIFIED'); + + // Check that the rule has not been updated const { body: setIndexRule } = await fetchRule(ruleId).expect(200); expect(setIndexRule.index).to.eql(undefined); @@ -1943,7 +2128,12 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(setIndexBody.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(setIndexBody.attributes.summary).to.eql({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); // Check that the updated rule is returned with the response expect(setIndexBody.attributes.results.updated[0].index).to.eql(['initial-index-*']); @@ -1979,7 +2169,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); expect(body.attributes.errors[0]).to.eql({ message: "Mutated params invalid: Index patterns can't be empty", status_code: 500, @@ -1992,15 +2182,14 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('should NOT set an index pattern to a rule and overwrite the data view when overwrite_data_views is false', async () => { + it('should return skipped rule and NOT set an index pattern to a rule or overwrite the data view when overwrite_data_views is false', async () => { const ruleId = 'ruleId'; const dataViewId = 'index1-*'; - const simpleRule = { + const simpleRule = await createRule(supertest, log, { ...getSimpleRule(ruleId), index: undefined, data_view_id: dataViewId, - }; - await createRule(supertest, log, simpleRule); + }); const { body: setIndexBody } = await postBulkAction() .send({ @@ -2016,13 +2205,21 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(setIndexBody.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(setIndexBody.attributes.summary).to.eql({ + failed: 0, + skipped: 1, + succeeded: 0, + total: 1, + }); - // Check that the updated rule is returned with the response - expect(setIndexBody.attributes.results.updated[0].index).to.eql(undefined); - expect(setIndexBody.attributes.results.updated[0].data_view_id).to.eql(dataViewId); + expect(setIndexBody.attributes.errors).to.be(undefined); - // Check that the updates have been persisted + // Check that the skipped rule is returned with the response + expect(setIndexBody.attributes.results.skipped[0].id).to.eql(simpleRule.id); + expect(setIndexBody.attributes.results.skipped[0].name).to.eql(simpleRule.name); + expect(setIndexBody.attributes.results.skipped[0].skip_reason).to.eql('RULE_NOT_MODIFIED'); + + // Check that the rule has not been updated const { body: setIndexRule } = await fetchRule(ruleId).expect(200); expect(setIndexRule.index).to.eql(undefined); @@ -2031,7 +2228,7 @@ export default ({ getService }: FtrProviderContext): void => { // This rule will now not have a source defined - as has been the behavior of rules since the beginning // this rule will use the default index patterns on rule run - it('should NOT error if all index patterns removed from a rule with data views when no index patterns exist on the rule and overwrite_data_views is true', async () => { + it('should be successful on an attempt to remove index patterns from a rule with only a dataView (no index patterns exist on the rule), if overwrite_data_views is true', async () => { const dataViewId = 'index1-*'; const ruleId = 'ruleId'; const rule = await createRule(supertest, log, { @@ -2054,7 +2251,12 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).to.eql({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); // Check that the updated rule is returned with the response expect(body.attributes.results.updated[0].index).to.eql(undefined); @@ -2090,7 +2292,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).to.eql({ failed: 1, succeeded: 0, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); expect(body.attributes.errors[0]).to.eql({ message: "Mutated params invalid: Index patterns can't be empty", status_code: 500, @@ -2103,7 +2305,7 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('should NOT return error if all index patterns removed from a rule with data views and overwrite_data_views is false', async () => { + it('should return a skipped rule if all index patterns removed from a rule with data views and overwrite_data_views is false', async () => { const dataViewId = 'index1-*'; const ruleId = 'ruleId'; const rule = await createRule(supertest, log, { @@ -2126,17 +2328,134 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 1, succeeded: 0, total: 1 }); + expect(body.attributes.errors).to.be(undefined); + + // Check that the skipped rule is returned with the response + expect(body.attributes.results.skipped[0].id).to.eql(rule.id); + expect(body.attributes.results.skipped[0].name).to.eql(rule.name); + expect(body.attributes.results.skipped[0].skip_reason).to.eql('RULE_NOT_MODIFIED'); + }); + }); + + describe('multiple_actions', () => { + it('should return one updated rule when applying two valid operations on a rule', async () => { + const ruleId = 'ruleId'; + const rule = await createRule(supertest, log, { + ...getSimpleRule(ruleId), + index: ['index1-*'], + tags: ['tag1', 'tag2'], + }); + + const { body } = await postBulkAction() + .send({ + ids: [rule.id], + action: BulkActionType.edit, + [BulkActionType.edit]: [ + { + type: BulkActionEditType.add_index_patterns, + value: ['initial-index-*'], + }, + { + type: BulkActionEditType.add_tags, + value: ['tag3'], + }, + ], + }) + .expect(200); + + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].index).to.eql(['simple-index-*']); - expect(body.attributes.results.updated[0].data_view_id).to.eql(dataViewId); + expect(body.attributes.results.updated[0].tags).to.eql(['tag1', 'tag2', 'tag3']); + expect(body.attributes.results.updated[0].index).to.eql(['index1-*', 'initial-index-*']); - // Check that the updates have been persisted - const { body: setIndexRule } = await fetchRule(ruleId).expect(200); + // Check that the rule has been persisted + const { body: updatedRule } = await fetchRule(ruleId).expect(200); - expect(setIndexRule.index).to.eql(['simple-index-*']); - expect(setIndexRule.data_view_id).to.eql(dataViewId); + expect(updatedRule.index).to.eql(['index1-*', 'initial-index-*']); + expect(updatedRule.tags).to.eql(['tag1', 'tag2', 'tag3']); + }); + + it('should return one updated rule when applying one valid operation and one operation to be skipped on a rule', async () => { + const ruleId = 'ruleId'; + const rule = await createRule(supertest, log, { + ...getSimpleRule(ruleId), + index: ['index1-*'], + tags: ['tag1', 'tag2'], + }); + + const { body } = await postBulkAction() + .send({ + ids: [rule.id], + action: BulkActionType.edit, + [BulkActionType.edit]: [ + // Valid operation + { + type: BulkActionEditType.add_index_patterns, + value: ['initial-index-*'], + }, + // Operation to be skipped + { + type: BulkActionEditType.add_tags, + value: ['tag1'], + }, + ], + }) + .expect(200); + + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + + // Check that the updated rule is returned with the response + expect(body.attributes.results.updated[0].tags).to.eql(['tag1', 'tag2']); + expect(body.attributes.results.updated[0].index).to.eql(['index1-*', 'initial-index-*']); + + // Check that the rule has been persisted + const { body: updatedRule } = await fetchRule(ruleId).expect(200); + + expect(updatedRule.index).to.eql(['index1-*', 'initial-index-*']); + expect(updatedRule.tags).to.eql(['tag1', 'tag2']); + }); + + it('should return one skipped rule when two (all) operations result in a no-op', async () => { + const ruleId = 'ruleId'; + const rule = await createRule(supertest, log, { + ...getSimpleRule(ruleId), + index: ['index1-*'], + tags: ['tag1', 'tag2'], + }); + + const { body } = await postBulkAction() + .send({ + ids: [rule.id], + action: BulkActionType.edit, + [BulkActionType.edit]: [ + // Operation to be skipped + { + type: BulkActionEditType.add_index_patterns, + value: ['index1-*'], + }, + // Operation to be skipped + { + type: BulkActionEditType.add_tags, + value: ['tag1'], + }, + ], + }) + .expect(200); + + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 1, succeeded: 0, total: 1 }); + + // Check that the skipped rule is returned with the response + expect(body.attributes.results.skipped[0].name).to.eql(rule.name); + expect(body.attributes.results.skipped[0].id).to.eql(rule.id); + expect(body.attributes.results.skipped[0].skip_reason).to.eql('RULE_NOT_MODIFIED'); + + // Check that no change to the rule have been persisted + const { body: skippedRule } = await fetchRule(ruleId).expect(200); + + expect(skippedRule.index).to.eql(['index1-*']); + expect(skippedRule.tags).to.eql(['tag1', 'tag2']); }); }); @@ -2192,7 +2511,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).to.eql({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the updated rule is returned with the response expect(body.attributes.results.updated[0].timeline_id).to.eql(timelineId); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action_dry_run.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action_dry_run.ts index 7914edf10247a..1996dc2438580 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action_dry_run.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action_dry_run.ts @@ -26,9 +26,9 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const es = getService('es'); const supertest = getService('supertest'); const log = getService('log'); + const es = getService('es'); const postDryRunBulkAction = () => supertest @@ -74,9 +74,14 @@ export default ({ getService }: FtrProviderContext): void => { .send({ action: BulkActionType.delete }) .expect(200); - expect(body.attributes.summary).toEqual({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // dry_run mode shouldn't return any rules in results - expect(body.attributes.results).toEqual({ updated: [], created: [], deleted: [] }); + expect(body.attributes.results).toEqual({ + updated: [], + skipped: [], + created: [], + deleted: [], + }); // Check that rule wasn't deleted await fetchRule(ruleId).expect(200); @@ -90,9 +95,14 @@ export default ({ getService }: FtrProviderContext): void => { .send({ action: BulkActionType.enable }) .expect(200); - expect(body.attributes.summary).toEqual({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // dry_run mode shouldn't return any rules in results - expect(body.attributes.results).toEqual({ updated: [], created: [], deleted: [] }); + expect(body.attributes.results).toEqual({ + updated: [], + skipped: [], + created: [], + deleted: [], + }); // Check that the updates have not been persisted const { body: ruleBody } = await fetchRule(ruleId).expect(200); @@ -107,9 +117,14 @@ export default ({ getService }: FtrProviderContext): void => { .send({ action: BulkActionType.disable }) .expect(200); - expect(body.attributes.summary).toEqual({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // dry_run mode shouldn't return any rules in results - expect(body.attributes.results).toEqual({ updated: [], created: [], deleted: [] }); + expect(body.attributes.results).toEqual({ + updated: [], + skipped: [], + created: [], + deleted: [], + }); // Check that the updates have not been persisted const { body: ruleBody } = await fetchRule(ruleId).expect(200); @@ -125,9 +140,14 @@ export default ({ getService }: FtrProviderContext): void => { .send({ action: BulkActionType.disable }) .expect(200); - expect(body.attributes.summary).toEqual({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // dry_run mode shouldn't return any rules in results - expect(body.attributes.results).toEqual({ updated: [], created: [], deleted: [] }); + expect(body.attributes.results).toEqual({ + updated: [], + skipped: [], + created: [], + deleted: [], + }); // Check that the rule wasn't duplicated const { body: rulesResponse } = await findRules().expect(200); @@ -153,9 +173,14 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - expect(body.attributes.summary).toEqual({ failed: 0, succeeded: 1, total: 1 }); + expect(body.attributes.summary).toEqual({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // dry_run mode shouldn't return any rules in results - expect(body.attributes.results).toEqual({ updated: [], created: [], deleted: [] }); + expect(body.attributes.results).toEqual({ + updated: [], + skipped: [], + created: [], + deleted: [], + }); // Check that the updates have not been persisted const { body: ruleBody } = await fetchRule(ruleId).expect(200); @@ -184,8 +209,13 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).toEqual({ failed: 1, succeeded: 0, total: 1 }); - expect(body.attributes.results).toEqual({ updated: [], created: [], deleted: [] }); + expect(body.attributes.summary).toEqual({ failed: 1, skipped: 0, succeeded: 0, total: 1 }); + expect(body.attributes.results).toEqual({ + updated: [], + skipped: [], + created: [], + deleted: [], + }); expect(body.attributes.errors).toHaveLength(1); expect(body.attributes.errors[0]).toEqual({ @@ -225,8 +255,18 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(500); - expect(body.attributes.summary).toEqual({ failed: 1, succeeded: 0, total: 1 }); - expect(body.attributes.results).toEqual({ updated: [], created: [], deleted: [] }); + expect(body.attributes.summary).toEqual({ + failed: 1, + skipped: 0, + succeeded: 0, + total: 1, + }); + expect(body.attributes.results).toEqual({ + updated: [], + skipped: [], + created: [], + deleted: [], + }); expect(body.attributes.errors).toHaveLength(1); expect(body.attributes.errors[0]).toEqual({ From 2b40b3c8487e1e7802fc29d3371da1c15168028d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 20 Dec 2022 11:40:23 +0000 Subject: [PATCH 48/55] [TableListView] Fix url state when Router context missing (#147841) --- .../table_list/src/table_list_view.tsx | 10 ++++++- .../table_list/src/use_url_state.ts | 9 ++++++ src/plugins/files_management/public/app.tsx | 4 +-- .../public/mount_management_section.tsx | 7 +++-- test/functional/apps/management/_files.ts | 29 +++++++++++++++++++ test/functional/apps/management/index.ts | 1 + test/functional/config.base.js | 3 ++ .../page_objects/files_management.ts | 17 +++++++++++ test/functional/page_objects/index.ts | 2 ++ 9 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 test/functional/apps/management/_files.ts create mode 100644 test/functional/page_objects/files_management.ts diff --git a/packages/content-management/table_list/src/table_list_view.tsx b/packages/content-management/table_list/src/table_list_view.tsx index 9342a5076a38f..41e26544a41d5 100644 --- a/packages/content-management/table_list/src/table_list_view.tsx +++ b/packages/content-management/table_list/src/table_list_view.tsx @@ -41,7 +41,7 @@ import type { SavedObjectsReference, SavedObjectsFindOptionsReference } from './ import { getReducer } from './reducer'; import type { SortColumnField } from './components'; import { useTags } from './use_tags'; -import { useUrlState } from './use_url_state'; +import { useInRouterContext, useUrlState } from './use_url_state'; interface ContentEditorConfig extends Pick { @@ -274,6 +274,14 @@ function TableListViewComp({ const openContentEditor = useOpenContentEditor(); + const isInRouterContext = useInRouterContext(); + + if (!isInRouterContext) { + throw new Error( + ` requires a React Router context. Ensure your component or React root is being rendered in the context of a .` + ); + } + const [urlState, setUrlState] = useUrlState({ queryParamsDeserializer: urlStateDeserializer, queryParamsSerializer: urlStateSerializer, diff --git a/packages/content-management/table_list/src/use_url_state.ts b/packages/content-management/table_list/src/use_url_state.ts index 2406ed597fbb9..353a068548945 100644 --- a/packages/content-management/table_list/src/use_url_state.ts +++ b/packages/content-management/table_list/src/use_url_state.ts @@ -9,6 +9,15 @@ import queryString from 'query-string'; import { useCallback, useMemo, useState, useEffect } from 'react'; import { useLocation, useHistory } from 'react-router-dom'; +export function useInRouterContext() { + try { + useLocation(); + return true; + } catch (e: unknown) { + return false; + } +} + function useQuery = {}>() { const { search } = useLocation(); return useMemo(() => queryString.parse(search) as T, [search]); diff --git a/src/plugins/files_management/public/app.tsx b/src/plugins/files_management/public/app.tsx index 2ce0a09ae3893..becdd05fa0e2c 100644 --- a/src/plugins/files_management/public/app.tsx +++ b/src/plugins/files_management/public/app.tsx @@ -27,7 +27,7 @@ export const App: FunctionComponent = () => { const [showDiagnosticsFlyout, setShowDiagnosticsFlyout] = useState(false); const [selectedFile, setSelectedFile] = useState(undefined); return ( - <> +
    tableListTitle={i18nTexts.tableListTitle} tableListDescription={i18nTexts.tableListDescription} @@ -78,6 +78,6 @@ export const App: FunctionComponent = () => { {Boolean(selectedFile) && ( setSelectedFile(undefined)} /> )} - +
    ); }; diff --git a/src/plugins/files_management/public/mount_management_section.tsx b/src/plugins/files_management/public/mount_management_section.tsx index 44ed8052c9a1f..259053d3da89b 100755 --- a/src/plugins/files_management/public/mount_management_section.tsx +++ b/src/plugins/files_management/public/mount_management_section.tsx @@ -8,6 +8,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { Router, Route } from 'react-router-dom'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { I18nProvider, FormattedRelative } from '@kbn/i18n-react'; import type { CoreStart } from '@kbn/core/public'; @@ -26,7 +27,7 @@ const queryClient = new QueryClient(); export const mountManagementSection = ( coreStart: CoreStart, startDeps: StartDependencies, - { element }: ManagementAppMountParams + { element, history }: ManagementAppMountParams ) => { ReactDOM.render( @@ -41,7 +42,9 @@ export const mountManagementSection = ( - + + + diff --git a/test/functional/apps/management/_files.ts b/test/functional/apps/management/_files.ts new file mode 100644 index 0000000000000..b117e376a39a6 --- /dev/null +++ b/test/functional/apps/management/_files.ts @@ -0,0 +1,29 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'filesManagement']); + const testSubjects = getService('testSubjects'); + + describe('Files management', () => { + before(async () => { + await PageObjects.filesManagement.navigateTo(); + }); + + it(`should render an empty prompt`, async () => { + await testSubjects.existOrFail('filesManagementApp'); + + const pageText = await (await testSubjects.find('filesManagementApp')).getVisibleText(); + + expect(pageText).to.contain('No files found'); + }); + }); +} diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index d38a94d1ab1ca..fb5e4145d356b 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -45,5 +45,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_handle_not_found')); loadTestFile(require.resolve('./_data_view_relationships')); loadTestFile(require.resolve('./_edit_field')); + loadTestFile(require.resolve('./_files')); }); } diff --git a/test/functional/config.base.js b/test/functional/config.base.js index d543f13755cce..aadaae3c8d81b 100644 --- a/test/functional/config.base.js +++ b/test/functional/config.base.js @@ -69,6 +69,9 @@ export default async function ({ readConfigFile }) { management: { pathname: '/app/management', }, + filesManagement: { + pathname: '/app/management/kibana/filesManagement', + }, /** @obsolete "management" should be instead of "settings" **/ settings: { pathname: '/app/management', diff --git a/test/functional/page_objects/files_management.ts b/test/functional/page_objects/files_management.ts new file mode 100644 index 0000000000000..e1a79b0639100 --- /dev/null +++ b/test/functional/page_objects/files_management.ts @@ -0,0 +1,17 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrService } from '../ftr_provider_context'; + +export class FilesManagementPageObject extends FtrService { + private readonly common = this.ctx.getPageObject('common'); + + async navigateTo() { + await this.common.navigateToApp('filesManagement'); + } +} diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts index bdfe91efef900..cf5a9f49151f2 100644 --- a/test/functional/page_objects/index.ts +++ b/test/functional/page_objects/index.ts @@ -32,6 +32,7 @@ import { LegacyDataTableVisPageObject } from './legacy/data_table_vis'; import { IndexPatternFieldEditorPageObject } from './management/indexpattern_field_editor_page'; import { DashboardPageControls } from './dashboard_page_controls'; import { UnifiedSearchPageObject } from './unified_search_page'; +import { FilesManagementPageObject } from './files_management'; export const pageObjects = { common: CommonPageObject, @@ -60,4 +61,5 @@ export const pageObjects = { savedObjects: SavedObjectsPageObject, indexPatternFieldEditorObjects: IndexPatternFieldEditorPageObject, unifiedSearch: UnifiedSearchPageObject, + filesManagement: FilesManagementPageObject, }; From 4226e6788e54ea1606a1269c04d468633aeade56 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 20 Dec 2022 13:45:35 +0200 Subject: [PATCH 49/55] [Lens] Displays the annotation icon on the annotation dimension label (#147686) ## Summary Closes https://github.com/elastic/kibana/issues/147329 Displays the icon/color on the annotations dimension panel label for better visibility. image I decided to take it one step further and apply the same logic to the reference layer. image Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../config_panel/color_indicator.tsx | 23 ++++++++++---- .../config_panel/palette_indicator.tsx | 2 +- x-pack/plugins/lens/public/types.ts | 10 ++++++- .../datatable/visualization.tsx | 4 +-- .../gauge/visualization.test.ts | 6 ++-- .../visualizations/gauge/visualization.tsx | 4 +-- .../heatmap/visualization.test.ts | 4 +-- .../visualizations/heatmap/visualization.tsx | 4 +-- .../legacy_metric/visualization.test.ts | 4 +-- .../legacy_metric/visualization.tsx | 2 +- .../__snapshots__/visualization.test.ts.snap | 4 +-- .../metric/visualization.test.ts | 12 ++++---- .../visualizations/metric/visualization.tsx | 8 ++--- .../partition/visualization.test.ts | 30 +++++++++---------- .../partition/visualization.tsx | 14 ++++----- .../visualizations/xy/annotations/helpers.tsx | 27 ++++++++++++----- .../visualizations/xy/color_assignment.ts | 16 +++++----- .../xy/reference_line_helpers.tsx | 15 +++++++--- .../visualizations/xy/visualization.test.ts | 17 +++++++---- .../visualizations/xy/visualization.tsx | 2 +- 20 files changed, 125 insertions(+), 83 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx index b12f50a7b35a0..c9cb591aae0e1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx @@ -18,7 +18,7 @@ export function ColorIndicator({ children: React.ReactChild; }) { let indicatorIcon = null; - if (accessorConfig.triggerIcon && accessorConfig.triggerIcon !== 'none') { + if (accessorConfig.triggerIconType && accessorConfig.triggerIconType !== 'none') { const baseIconProps = { size: 's', className: 'lnsLayerPanel__colorIndicator', @@ -26,7 +26,7 @@ export function ColorIndicator({ indicatorIcon = ( - {accessorConfig.triggerIcon === 'color' && accessorConfig.color && ( + {accessorConfig.triggerIconType === 'color' && accessorConfig.color && ( )} - {accessorConfig.triggerIcon === 'disabled' && ( + {accessorConfig.triggerIconType === 'disabled' && ( )} - {accessorConfig.triggerIcon === 'invisible' && ( + {accessorConfig.triggerIconType === 'invisible' && ( )} - {accessorConfig.triggerIcon === 'aggregate' && ( + {accessorConfig.triggerIconType === 'aggregate' && ( )} - {accessorConfig.triggerIcon === 'colorBy' && ( + {accessorConfig.triggerIconType === 'colorBy' && ( )} + {accessorConfig.triggerIconType === 'custom' && accessorConfig.customIcon && ( + + )} ); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx index 25336e8974b45..21a44bceed788 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx @@ -10,7 +10,7 @@ import { EuiColorPaletteDisplay } from '@elastic/eui'; import { AccessorConfig } from '../../../types'; export function PaletteIndicator({ accessorConfig }: { accessorConfig: AccessorConfig }) { - if (accessorConfig.triggerIcon !== 'colorBy' || !accessorConfig.palette) return null; + if (accessorConfig.triggerIconType !== 'colorBy' || !accessorConfig.palette) return null; return (
    = VisualizationConfig export interface AccessorConfig { columnId: string; - triggerIcon?: 'color' | 'disabled' | 'colorBy' | 'none' | 'invisible' | 'aggregate'; + triggerIconType?: + | 'color' + | 'disabled' + | 'colorBy' + | 'none' + | 'invisible' + | 'aggregate' + | 'custom'; + customIcon?: IconType; color?: string; palette?: string[] | Array<{ color: string; stop: number }>; } diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx index e243da166b860..9e31b0bd0035e 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx @@ -226,7 +226,7 @@ export const getDatatableVisualization = ({ ) .map((accessor) => ({ columnId: accessor, - triggerIcon: columnMap[accessor].hidden + triggerIconType: columnMap[accessor].hidden ? 'invisible' : columnMap[accessor].collapseFn ? 'aggregate' @@ -289,7 +289,7 @@ export const getDatatableVisualization = ({ return { columnId: accessor, - triggerIcon: columnConfig?.hidden + triggerIconType: columnConfig?.hidden ? 'invisible' : hasColoring ? 'colorBy' diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts index 1041a7bb36078..29e14c9412bd1 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts @@ -103,7 +103,7 @@ describe('gauge', () => { groupId: GROUP_ID.METRIC, groupLabel: 'Metric', isMetricDimension: true, - accessors: [{ columnId: 'metric-accessor', triggerIcon: 'none' }], + accessors: [{ columnId: 'metric-accessor', triggerIconType: 'none' }], filterOperations: isNumericDynamicMetric, supportsMoreColumns: false, requiredMinDimensionCount: 1, @@ -281,7 +281,7 @@ describe('gauge', () => { groupId: GROUP_ID.METRIC, groupLabel: 'Metric', isMetricDimension: true, - accessors: [{ columnId: 'metric-accessor', triggerIcon: 'none' }], + accessors: [{ columnId: 'metric-accessor', triggerIconType: 'none' }], filterOperations: isNumericDynamicMetric, supportsMoreColumns: false, requiredMinDimensionCount: 1, @@ -378,7 +378,7 @@ describe('gauge', () => { groupId: GROUP_ID.METRIC, groupLabel: 'Metric', isMetricDimension: true, - accessors: [{ columnId: 'metric-accessor', triggerIcon: 'none' }], + accessors: [{ columnId: 'metric-accessor', triggerIconType: 'none' }], filterOperations: isNumericDynamicMetric, supportsMoreColumns: false, requiredMinDimensionCount: 1, diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx index 4f66f60131be2..26077fd45c566 100644 --- a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -250,12 +250,12 @@ export const getGaugeVisualization = ({ palette ? { columnId: metricAccessor, - triggerIcon: 'colorBy', + triggerIconType: 'colorBy', palette, } : { columnId: metricAccessor, - triggerIcon: 'none', + triggerIconType: 'none', }, ] : [], diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts index e26fe130e8da9..7f6a9caf506fd 100644 --- a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.test.ts @@ -160,7 +160,7 @@ describe('heatmap', () => { accessors: [ { columnId: 'v-accessor', - triggerIcon: 'colorBy', + triggerIconType: 'colorBy', palette: ['blue', 'yellow'], }, ], @@ -276,7 +276,7 @@ describe('heatmap', () => { accessors: [ { columnId: 'v-accessor', - triggerIcon: 'none', + triggerIconType: 'none', }, ], filterOperations: isCellValueSupported, diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx index 9b8ad6f7ed616..1db54423bf1a0 100644 --- a/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/heatmap/visualization.tsx @@ -219,12 +219,12 @@ export const getHeatmapVisualization = ({ (frame.activeData || activePalette?.params?.rangeType !== 'number') ? { columnId: state.valueAccessor, - triggerIcon: 'colorBy', + triggerIconType: 'colorBy', palette: displayStops.map(({ color }) => color), } : { columnId: state.valueAccessor, - triggerIcon: 'none', + triggerIconType: 'none', }, ] : [], diff --git a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts index 6724b9aafa0d1..ebb84b8da1b71 100644 --- a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.test.ts @@ -138,7 +138,7 @@ describe('metric_visualization', () => { groups: [ expect.objectContaining({ accessors: expect.arrayContaining([ - { columnId: 'a', triggerIcon: 'colorBy', palette: [] }, + { columnId: 'a', triggerIconType: 'colorBy', palette: [] }, ]), }), ], @@ -160,7 +160,7 @@ describe('metric_visualization', () => { groups: [ expect.objectContaining({ accessors: expect.arrayContaining([ - { columnId: 'a', triggerIcon: undefined, palette: undefined }, + { columnId: 'a', triggerIconType: undefined, palette: undefined }, ]), }), ], diff --git a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx index d5a1c3d5918f0..499003dd87319 100644 --- a/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/legacy_metric/visualization.tsx @@ -232,7 +232,7 @@ export const getLegacyMetricVisualization = ({ ? [ { columnId: props.state.accessor, - triggerIcon: hasColoring ? 'colorBy' : undefined, + triggerIconType: hasColoring ? 'colorBy' : undefined, palette: hasColoring ? stops.map(({ color }) => color) : undefined, }, ] diff --git a/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap b/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap index 64b509e5f7a8c..904f33f111364 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap +++ b/x-pack/plugins/lens/public/visualizations/metric/__snapshots__/visualization.test.ts.snap @@ -8,7 +8,7 @@ Object { Object { "columnId": "metric-col-id", "palette": Array [], - "triggerIcon": "colorBy", + "triggerIconType": "colorBy", }, ], "dataTestSubj": "lnsMetric_primaryMetricDimensionPanel", @@ -75,7 +75,7 @@ Object { "accessors": Array [ Object { "columnId": "breakdown-col-id", - "triggerIcon": "aggregate", + "triggerIconType": "aggregate", }, ], "dataTestSubj": "lnsMetric_breakdownByDimensionPanel", diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts index 41e9fab67d4d6..314c175d24fbe 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts @@ -120,7 +120,7 @@ describe('metric visualization', () => { Object { "columnId": "metric-col-id", "palette": Array [], - "triggerIcon": "colorBy", + "triggerIconType": "colorBy", }, ] `); @@ -136,7 +136,7 @@ describe('metric visualization', () => { Object { "color": "#f5f7fa", "columnId": "metric-col-id", - "triggerIcon": "color", + "triggerIconType": "color", }, ] `); @@ -154,7 +154,7 @@ describe('metric visualization', () => { Object { "color": "static-color", "columnId": "metric-col-id", - "triggerIcon": "color", + "triggerIconType": "color", }, ] `); @@ -170,7 +170,7 @@ describe('metric visualization', () => { Object { "columnId": "metric-col-id", "palette": Array [], - "triggerIcon": "colorBy", + "triggerIconType": "colorBy", }, ] `); @@ -187,7 +187,7 @@ describe('metric visualization', () => { Array [ Object { "columnId": "breakdown-col-id", - "triggerIcon": "aggregate", + "triggerIconType": "aggregate", }, ] `); @@ -202,7 +202,7 @@ describe('metric visualization', () => { Array [ Object { "columnId": "breakdown-col-id", - "triggerIcon": undefined, + "triggerIconType": undefined, }, ] `); diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx index d43cf3d668b48..2a77ce35be48c 100644 --- a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx @@ -98,16 +98,16 @@ const getMetricLayerConfiguration = ( const hasDynamicColoring = !!props.state.palette; return hasDynamicColoring ? { - triggerIcon: 'colorBy', + triggerIconType: 'colorBy', palette: stops.map(({ color }) => color), } : hasStaticColoring ? { - triggerIcon: 'color', + triggerIconType: 'color', color: props.state.color, } : { - triggerIcon: 'color', + triggerIconType: 'color', color: getDefaultColor(props.state), }; }; @@ -209,7 +209,7 @@ const getMetricLayerConfiguration = ( ? [ { columnId: props.state.breakdownByAccessor, - triggerIcon: props.state.collapseFn ? ('aggregate' as const) : undefined, + triggerIconType: props.state.collapseFn ? ('aggregate' as const) : undefined, }, ] : [], diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts index d77466c214d67..8617a2f0f7664 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.test.ts @@ -260,7 +260,7 @@ describe('pie_visualization', () => { Array [ Object { "columnId": "1", - "triggerIcon": "aggregate", + "triggerIconType": "aggregate", }, Object { "columnId": "2", @@ -268,15 +268,15 @@ describe('pie_visualization', () => { "red", "black", ], - "triggerIcon": "colorBy", + "triggerIconType": "colorBy", }, Object { "columnId": "3", - "triggerIcon": "aggregate", + "triggerIconType": "aggregate", }, Object { "columnId": "4", - "triggerIcon": undefined, + "triggerIconType": undefined, }, ] `); @@ -302,7 +302,7 @@ describe('pie_visualization', () => { Array [ Object { "columnId": "1", - "triggerIcon": "aggregate", + "triggerIconType": "aggregate", }, Object { "columnId": "2", @@ -310,17 +310,17 @@ describe('pie_visualization', () => { "red", "black", ], - "triggerIcon": "colorBy", + "triggerIconType": "colorBy", }, ], Array [ Object { "columnId": "3", - "triggerIcon": "aggregate", + "triggerIconType": "aggregate", }, Object { "columnId": "4", - "triggerIcon": undefined, + "triggerIconType": undefined, }, ], Array [], @@ -348,22 +348,22 @@ describe('pie_visualization', () => { Object { "color": "overridden-color", "columnId": "1", - "triggerIcon": "color", + "triggerIconType": "color", }, Object { "color": "black", "columnId": "2", - "triggerIcon": "color", + "triggerIconType": "color", }, Object { "color": "black", "columnId": "3", - "triggerIcon": "color", + "triggerIconType": "color", }, Object { "color": "black", "columnId": "4", - "triggerIcon": "color", + "triggerIconType": "color", }, ], ] @@ -422,15 +422,15 @@ describe('pie_visualization', () => { Array [ Object { "columnId": "2", - "triggerIcon": "disabled", + "triggerIconType": "disabled", }, Object { "columnId": "3", - "triggerIcon": "disabled", + "triggerIconType": "disabled", }, Object { "columnId": "4", - "triggerIcon": "disabled", + "triggerIconType": "disabled", }, ] `); diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx index 48cbff186e3de..4c214de970e82 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx @@ -168,16 +168,16 @@ export const getPieVisualization = ({ const getPrimaryGroupConfig = (): VisualizationDimensionGroupConfig => { const originalOrder = getSortedGroups(datasource, layer); // When we add a column it could be empty, and therefore have no order - const accessors: AccessorConfig[] = originalOrder.map((accessor) => ({ + const accessors = originalOrder.map((accessor) => ({ columnId: accessor, - triggerIcon: isCollapsed(accessor, layer) ? ('aggregate' as const) : undefined, + triggerIconType: isCollapsed(accessor, layer) ? 'aggregate' : undefined, })); const firstNonCollapsedColumnId = layer.primaryGroups.find((id) => !isCollapsed(id, layer)); accessors.forEach((accessorConfig) => { if (firstNonCollapsedColumnId === accessorConfig.columnId) { - accessorConfig.triggerIcon = 'colorBy'; + accessorConfig.triggerIconType = 'colorBy'; accessorConfig.palette = paletteService .get(state.palette?.name || 'default') .getCategoricalColors(10, state.palette?.params); @@ -273,9 +273,9 @@ export const getPieVisualization = ({ const getSecondaryGroupConfig = (): VisualizationDimensionGroupConfig | undefined => { const originalSecondaryOrder = getSortedGroups(datasource, layer, 'secondaryGroups'); - const accessors = originalSecondaryOrder.map((accessor) => ({ + const accessors = originalSecondaryOrder.map((accessor) => ({ columnId: accessor, - triggerIcon: isCollapsed(accessor, layer) ? ('aggregate' as const) : undefined, + triggerIconType: isCollapsed(accessor, layer) ? 'aggregate' : undefined, })); const secondaryGroupConfigBaseProps = { @@ -321,10 +321,10 @@ export const getPieVisualization = ({ ...(layer.allowMultipleMetrics ? hasSliceBy ? { - triggerIcon: 'disabled', + triggerIconType: 'disabled', } : { - triggerIcon: 'color', + triggerIconType: 'color', color: layer.colorsByDimension?.[columnId] ?? getDefaultColorForMultiMetricDimension({ diff --git a/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx b/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx index 7d2940f5b0df5..9d1a7a33421e2 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx @@ -17,8 +17,9 @@ import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; import { IconChartBarAnnotations } from '@kbn/chart-icons'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { isDraggedDataViewField } from '../../../utils'; -import type { FramePublicAPI, Visualization } from '../../../types'; +import type { FramePublicAPI, Visualization, AccessorConfig } from '../../../types'; import { isHorizontalChart } from '../state_helpers'; +import { annotationsIconSet } from '../xy_config_panel/annotations_config_panel/icon_set'; import type { XYState, XYDataLayerConfig, XYAnnotationLayerConfig, XYLayerConfig } from '../types'; import { checkScaleOperation, @@ -444,13 +445,23 @@ export const setAnnotationsDimension: Visualization['setDimension'] = ( }; }; -export const getSingleColorAnnotationConfig = (annotation: EventAnnotationConfig) => ({ - columnId: annotation.id, - triggerIcon: annotation.isHidden ? ('invisible' as const) : ('color' as const), - color: - annotation?.color || - (isRangeAnnotationConfig(annotation) ? defaultAnnotationRangeColor : defaultAnnotationColor), -}); +export const getSingleColorAnnotationConfig = ( + annotation: EventAnnotationConfig +): AccessorConfig => { + const annotationIcon = !isRangeAnnotationConfig(annotation) + ? annotationsIconSet.find((option) => option.value === annotation?.icon) || + annotationsIconSet.find((option) => option.value === 'triangle') + : undefined; + const icon = annotationIcon?.icon ?? annotationIcon?.value; + return { + columnId: annotation.id, + triggerIconType: annotation.isHidden ? 'invisible' : icon ? 'custom' : 'color', + customIcon: icon, + color: + annotation?.color || + (isRangeAnnotationConfig(annotation) ? defaultAnnotationRangeColor : defaultAnnotationColor), + }; +}; export const getAnnotationsAccessorColorConfig = (layer: XYAnnotationLayerConfig) => layer.annotations.map((annotation) => getSingleColorAnnotationConfig(annotation)); diff --git a/x-pack/plugins/lens/public/visualizations/xy/color_assignment.ts b/x-pack/plugins/lens/public/visualizations/xy/color_assignment.ts index 3f5fa1afb9024..45742a72ca9de 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/color_assignment.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/color_assignment.ts @@ -104,10 +104,10 @@ export function getColorAssignments( }); } -function getDisabledConfig(accessor: string) { +function getDisabledConfig(accessor: string): AccessorConfig { return { - columnId: accessor as string, - triggerIcon: 'disabled' as const, + columnId: accessor, + triggerIconType: 'disabled', }; } @@ -125,7 +125,7 @@ export function getAssignedColorConfig( const annotation = layer.annotations.find((a) => a.id === accessor); return { columnId: accessor, - triggerIcon: annotation?.isHidden ? ('invisible' as const) : ('color' as const), + triggerIconType: annotation?.isHidden ? 'invisible' : 'color', color: isRangeAnnotationConfig(annotation) ? defaultAnnotationRangeColor : defaultAnnotationColor, @@ -160,8 +160,8 @@ export function getAssignedColorConfig( ) : undefined; return { - columnId: accessor as string, - triggerIcon: assignedColor ? 'color' : 'disabled', + columnId: accessor, + triggerIconType: assignedColor ? 'color' : 'disabled', color: assignedColor ?? undefined, }; } @@ -186,8 +186,8 @@ export function getAccessorColorConfigs( const currentYConfig = layer.yConfig?.find((yConfig) => yConfig.forAccessor === accessor); if (currentYConfig?.color) { return { - columnId: accessor as string, - triggerIcon: 'color', + columnId: accessor, + triggerIconType: 'color', color: currentYConfig.color, }; } diff --git a/x-pack/plugins/lens/public/visualizations/xy/reference_line_helpers.tsx b/x-pack/plugins/lens/public/visualizations/xy/reference_line_helpers.tsx index 6e1763841e70a..e53c371303739 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/reference_line_helpers.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/reference_line_helpers.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { Datatable } from '@kbn/expressions-plugin/public'; import { IconChartBarReferenceLine } from '@kbn/chart-icons'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; -import type { DatasourceLayers, FramePublicAPI, Visualization } from '../../types'; +import type { DatasourceLayers, FramePublicAPI, Visualization, AccessorConfig } from '../../types'; import { groupAxesByType } from './axes_configuration'; import { isHorizontalChart, isPercentageSeries, isStackedChart } from './state_helpers'; import type { @@ -379,10 +379,15 @@ export const setReferenceDimension: Visualization['setDimension'] = ({ }; }; -export const getSingleColorConfig = (id: string, color = defaultReferenceLineColor) => ({ +export const getSingleColorConfig = ( + id: string, + color = defaultReferenceLineColor, + icon?: string +): AccessorConfig => ({ columnId: id, - triggerIcon: 'color' as const, + triggerIconType: icon && icon !== 'empty' ? 'custom' : 'color', color, + customIcon: icon, }); export const getReferenceLineAccessorColorConfig = (layer: XYReferenceLineLayerConfig) => { @@ -458,7 +463,9 @@ export const getReferenceConfiguration = ({ values: { groupLabel: getAxisName(label, { isHorizontal }) }, } ), - accessors: config.map(({ forAccessor, color }) => getSingleColorConfig(forAccessor, color)), + accessors: config.map(({ forAccessor, color, icon }) => + getSingleColorConfig(forAccessor, color, icon) + ), filterOperations: isNumericMetric, supportsMoreColumns: true, requiredMinDimensionCount: 0, diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts index 275ecb3c24f34..2000f0714687f 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts @@ -17,7 +17,7 @@ import type { SeriesType, } from './types'; import { createMockDatasource, createMockFramePublicAPI } from '../../mocks'; -import { IconChartBar } from '@kbn/chart-icons'; +import { IconChartBar, IconCircle } from '@kbn/chart-icons'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; import { Datatable } from '@kbn/expressions-plugin/common'; @@ -1983,7 +1983,12 @@ describe('xy_visualization', () => { layerId: 'annotations', }); expect(config.groups[0].accessors).toEqual([ - { color: '#f04e98', columnId: 'an1', triggerIcon: 'color' }, + { + color: '#f04e98', + columnId: 'an1', + customIcon: IconCircle, + triggerIconType: 'custom', + }, ]); expect(config.groups[0].invalid).toEqual(false); }); @@ -2053,7 +2058,7 @@ describe('xy_visualization', () => { }, 'b' ); - expect(accessorConfig.triggerIcon).toEqual('color'); + expect(accessorConfig.triggerIconType).toEqual('color'); expect(accessorConfig.color).toEqual('red'); }); @@ -2061,7 +2066,7 @@ describe('xy_visualization', () => { const palette = paletteServiceMock.get('default'); (palette.getCategoricalColor as jest.Mock).mockClear(); const accessorConfig = callConfigAndFindYConfig({}, 'c'); - expect(accessorConfig.triggerIcon).toEqual('color'); + expect(accessorConfig.triggerIconType).toEqual('color'); // black is the color returned from the palette mock expect(accessorConfig.color).toEqual('black'); expect(palette.getCategoricalColor).toHaveBeenCalledWith( @@ -2113,7 +2118,7 @@ describe('xy_visualization', () => { const yConfigs = callConfigForYConfigs({}); expect(yConfigs!.accessors.length).toEqual(2); yConfigs!.accessors.forEach((accessor) => { - expect(accessor.triggerIcon).toBeUndefined(); + expect(accessor.triggerIconType).toBeUndefined(); }); }); @@ -2124,7 +2129,7 @@ describe('xy_visualization', () => { }, 'b' ); - expect(accessorConfig.triggerIcon).toEqual('disabled'); + expect(accessorConfig.triggerIconType).toEqual('disabled'); }); it('should show current palette for breakdown dimension', () => { diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index 766550f6eda06..9ee9572a0c35f 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -389,7 +389,7 @@ export const getXyVisualization = ({ ? [ { columnId: dataLayer.splitAccessor, - triggerIcon: dataLayer.collapseFn ? ('aggregate' as const) : ('colorBy' as const), + triggerIconType: dataLayer.collapseFn ? 'aggregate' : 'colorBy', palette: dataLayer.collapseFn ? undefined : paletteService From 5535dd1ad1cdc3d400da553fb2a7841a391f6358 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 20 Dec 2022 13:44:18 +0100 Subject: [PATCH 50/55] [UnifiedFieldList] Migrate field filters and field search from Lens (#147255) Closes https://github.com/elastic/kibana/issues/145089 and https://github.com/elastic/kibana/issues/147003 ## Summary This PR migrates field filters and field search from Lens plugin to UnifiedFieldList plugin: - [x] Move the components - [x] Update design to add help elements and counters - [x] Refactor Lens `form_based` and `text_based` views - [x] Extend `text_based` view with search highlights and type filters - [x] Refactor classes and data-test-subj - [x] Unify field icon and field type label code and move to Unified Field List - [x] Rename `timeSeriesMetricType` with `timeSeriesMetric` to be able to use this logic for both DataViewField and IndexPatternField - [x] Migrate `onFilterField` logic to Unified Field List - [x] Migrate `availableFieldTypes` logic to Unified Field List - [x] Create a unified FieldList wrapper component - [x] Update Lens tests if necessary - [x] Add tests and docs Screenshot 2022-12-16 at 19 11 14 Screenshot 2022-12-16 at 19 12 00 ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) Co-authored-by: Matthias Wilhelm --- .../components/sidebar/discover_field.tsx | 12 +- .../components/sidebar/discover_sidebar.tsx | 10 +- src/plugins/unified_field_list/README.md | 93 ++++-- .../__snapshots__/field_icon.test.tsx.snap | 40 +++ .../components/field_icon/field_icon.test.tsx | 43 +++ .../components/field_icon/field_icon.tsx | 31 ++ .../field_icon/get_field_icon_props.ts | 21 ++ .../public/components/field_icon/index.tsx | 28 ++ .../components/field_list/field_list.test.tsx | 45 +++ .../components/field_list/field_list.tsx | 65 +++++ .../public/components/field_list/index.ts | 9 + .../field_list_filters/field_list_filters.tsx | 87 ++++++ .../field_name_search.test.tsx | 53 ++++ .../field_list_filters/field_name_search.tsx | 57 ++++ .../field_type_filter.test.tsx | 138 +++++++++ .../field_list_filters/field_type_filter.tsx | 273 ++++++++++++++++++ .../components/field_list_filters/index.tsx | 31 ++ .../field_list_grouped.scss | 0 .../field_list_grouped.test.tsx | 96 +++--- .../field_list_grouped.tsx | 12 +- .../fields_accordion.scss | 0 .../fields_accordion.test.tsx | 16 +- .../fields_accordion.tsx | 12 +- .../index.tsx | 0 .../no_fields_callout.test.tsx | 0 .../no_fields_callout.tsx | 0 .../public/hooks/use_existing_fields.ts | 6 + .../public/hooks/use_field_filters.test.tsx | 113 ++++++++ .../public/hooks/use_field_filters.ts | 102 +++++++ .../public/hooks/use_grouped_fields.test.tsx | 184 +++++++++--- .../public/hooks/use_grouped_fields.ts | 79 +++-- .../unified_field_list/public/index.ts | 22 +- .../unified_field_list/public/types.ts | 11 + .../public/utils/field_types/field_types.ts | 41 +++ .../field_types/get_field_icon_type.test.ts | 48 +++ .../utils/field_types/get_field_icon_type.ts | 29 ++ .../utils/field_types/get_field_type.test.ts | 29 ++ .../utils/field_types/get_field_type.ts | 22 ++ .../get_field_type_description.test.ts | 35 +++ .../field_types/get_field_type_description.ts | 117 ++++++++ .../field_types/get_field_type_name.test.ts | 35 +++ .../utils/field_types/get_field_type_name.ts | 123 ++++++++ .../public/utils/field_types/index.ts | 13 + .../utils/wrap_field_name_on_dot.test.ts | 22 ++ .../public/utils/wrap_field_name_on_dot.ts | 19 ++ .../lens/public/data_views_service/loader.ts | 2 +- .../datasources/form_based/datapanel.scss | 25 -- .../datasources/form_based/datapanel.test.tsx | 26 +- .../datasources/form_based/datapanel.tsx | 223 ++------------ .../form_based/field_item.test.tsx | 6 +- .../datasources/form_based/field_item.tsx | 23 +- .../datasources/form_based/pure_utils.ts | 4 +- .../datasources/text_based/datapanel.test.tsx | 26 +- .../datasources/text_based/datapanel.tsx | 111 +++---- .../lens_field_icon.test.tsx.snap | 16 - .../field_picker/field_picker.tsx | 4 +- .../shared_components/field_picker/index.ts | 1 - .../field_picker/lens_field_icon.test.tsx | 20 -- .../field_picker/lens_field_icon.tsx | 21 -- .../lens/public/shared_components/index.ts | 2 +- x-pack/plugins/lens/public/types.ts | 4 +- .../translations/translations/fr-FR.json | 15 - .../translations/translations/ja-JP.json | 15 - .../translations/translations/zh-CN.json | 15 - .../test/functional/page_objects/lens_page.ts | 2 +- 65 files changed, 2170 insertions(+), 613 deletions(-) create mode 100644 src/plugins/unified_field_list/public/components/field_icon/__snapshots__/field_icon.test.tsx.snap create mode 100644 src/plugins/unified_field_list/public/components/field_icon/field_icon.test.tsx create mode 100644 src/plugins/unified_field_list/public/components/field_icon/field_icon.tsx create mode 100644 src/plugins/unified_field_list/public/components/field_icon/get_field_icon_props.ts create mode 100644 src/plugins/unified_field_list/public/components/field_icon/index.tsx create mode 100644 src/plugins/unified_field_list/public/components/field_list/field_list.test.tsx create mode 100644 src/plugins/unified_field_list/public/components/field_list/field_list.tsx create mode 100644 src/plugins/unified_field_list/public/components/field_list/index.ts create mode 100644 src/plugins/unified_field_list/public/components/field_list_filters/field_list_filters.tsx create mode 100644 src/plugins/unified_field_list/public/components/field_list_filters/field_name_search.test.tsx create mode 100644 src/plugins/unified_field_list/public/components/field_list_filters/field_name_search.tsx create mode 100644 src/plugins/unified_field_list/public/components/field_list_filters/field_type_filter.test.tsx create mode 100644 src/plugins/unified_field_list/public/components/field_list_filters/field_type_filter.tsx create mode 100644 src/plugins/unified_field_list/public/components/field_list_filters/index.tsx rename src/plugins/unified_field_list/public/components/{field_list => field_list_grouped}/field_list_grouped.scss (100%) rename src/plugins/unified_field_list/public/components/{field_list => field_list_grouped}/field_list_grouped.test.tsx (80%) rename src/plugins/unified_field_list/public/components/{field_list => field_list_grouped}/field_list_grouped.tsx (97%) rename src/plugins/unified_field_list/public/components/{field_list => field_list_grouped}/fields_accordion.scss (100%) rename src/plugins/unified_field_list/public/components/{field_list => field_list_grouped}/fields_accordion.test.tsx (78%) rename src/plugins/unified_field_list/public/components/{field_list => field_list_grouped}/fields_accordion.tsx (93%) rename src/plugins/unified_field_list/public/components/{field_list => field_list_grouped}/index.tsx (100%) rename src/plugins/unified_field_list/public/components/{field_list => field_list_grouped}/no_fields_callout.test.tsx (100%) rename src/plugins/unified_field_list/public/components/{field_list => field_list_grouped}/no_fields_callout.tsx (100%) create mode 100644 src/plugins/unified_field_list/public/hooks/use_field_filters.test.tsx create mode 100644 src/plugins/unified_field_list/public/hooks/use_field_filters.ts create mode 100644 src/plugins/unified_field_list/public/utils/field_types/field_types.ts create mode 100644 src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.test.ts create mode 100644 src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.ts create mode 100644 src/plugins/unified_field_list/public/utils/field_types/get_field_type.test.ts create mode 100644 src/plugins/unified_field_list/public/utils/field_types/get_field_type.ts create mode 100644 src/plugins/unified_field_list/public/utils/field_types/get_field_type_description.test.ts create mode 100644 src/plugins/unified_field_list/public/utils/field_types/get_field_type_description.ts create mode 100644 src/plugins/unified_field_list/public/utils/field_types/get_field_type_name.test.ts create mode 100644 src/plugins/unified_field_list/public/utils/field_types/get_field_type_name.ts create mode 100644 src/plugins/unified_field_list/public/utils/field_types/index.ts create mode 100644 src/plugins/unified_field_list/public/utils/wrap_field_name_on_dot.test.ts create mode 100644 src/plugins/unified_field_list/public/utils/wrap_field_name_on_dot.ts delete mode 100644 x-pack/plugins/lens/public/shared_components/field_picker/__snapshots__/lens_field_icon.test.tsx.snap delete mode 100644 x-pack/plugins/lens/public/shared_components/field_picker/lens_field_icon.test.tsx delete mode 100644 x-pack/plugins/lens/public/shared_components/field_picker/lens_field_icon.tsx diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx index 18cbabf97e058..89b143cf46cd3 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx @@ -27,6 +27,7 @@ import { FieldPopoverHeader, FieldPopoverHeaderProps, FieldPopoverVisualize, + wrapFieldNameOnDot, } from '@kbn/unified-field-list-plugin/public'; import { DiscoverFieldStats } from './discover_field_stats'; import { getTypeForFieldIcon } from '../../../../utils/get_type_for_field_icon'; @@ -37,13 +38,6 @@ import { SHOW_LEGACY_FIELD_TOP_VALUES, PLUGIN_ID } from '../../../../../common'; import { getUiActions } from '../../../../kibana_services'; import { type DataDocuments$ } from '../../hooks/use_saved_search'; -function wrapOnDot(str?: string) { - // u200B is a non-width white-space character, which allows - // the browser to efficiently word-wrap right after the dot - // without us having to draw a lot of extra DOM elements, etc - return str ? str.replace(/\./g, '.\u200B') : ''; -} - const FieldInfoIcon: React.FC = memo(() => ( = memo( return ( - {wrapOnDot(field.displayName)} + {wrapFieldNameOnDot(field.displayName)} ); } diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx index 109f11615335f..75bfdffa79627 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx @@ -25,7 +25,6 @@ import { FieldsGroupNames, GroupedFieldsParams, triggerVisualizeActionsTextBasedLanguages, - useExistingFieldsReader, useGroupedFields, } from '@kbn/unified-field-list-plugin/public'; import { useAppStateSelector } from '../../services/discover_app_state_container'; @@ -132,7 +131,7 @@ export function DiscoverSidebarComponent({ showFieldList, isAffectedByGlobalFilter, }: DiscoverSidebarProps) { - const { uiSettings, dataViewFieldEditor, dataViews } = useDiscoverServices(); + const { uiSettings, dataViewFieldEditor, dataViews, core } = useDiscoverServices(); const isPlainRecord = useAppStateSelector( (state) => getRawRecordType(state.query) === RecordRawType.PLAIN ); @@ -268,16 +267,15 @@ export function DiscoverSidebarComponent({ }; } }, []); - const fieldsExistenceReader = useExistingFieldsReader(); - const fieldListGroupedProps = useGroupedFields({ + const { fieldListGroupedProps } = useGroupedFields({ dataViewId: (!isPlainRecord && selectedDataView?.id) || null, // passing `null` for text-based queries - fieldsExistenceReader: !isPlainRecord ? fieldsExistenceReader : undefined, allFields, popularFieldsLimit: !isPlainRecord ? popularFieldsLimit : 0, sortedSelectedFields: selectedFieldsState.selectedFields, isAffectedByGlobalFilter, services: { dataViews, + core, }, onFilterField, onSupportedFieldFilter, @@ -378,7 +376,7 @@ export function DiscoverSidebarComponent({ )} diff --git a/src/plugins/unified_field_list/README.md b/src/plugins/unified_field_list/README.md index 78a6e5084691e..7f1ce38d6c4cf 100755 --- a/src/plugins/unified_field_list/README.md +++ b/src/plugins/unified_field_list/README.md @@ -4,10 +4,8 @@ This Kibana plugin contains components and services for field list UI (as in fie --- -## Components +## Field Stats and Field Popover Components -* `` - renders a fields list which is split in sections (Selected, Special, Available, Empty, Meta fields). It accepts already grouped fields, please use `useGroupedFields` hook for it. - * `` - loads and renders stats (Top values, Distribution) for a data view field. * `` - renders a button to open this field in Lens. @@ -55,6 +53,54 @@ These components can be combined and customized as the following: /> ``` +## Field List components + +* `` - a top-level component to render field filters and field list sections. + +* `` - renders a field search input and field filters by type. Please use `useGroupedFields` hook for it. For a more direct control, see `useFieldFilters` hook. + +* `` - renders a fields list which is split in sections (Special, Selected, Popular, Available, Empty, Meta fields). It accepts already grouped fields, please use `useGroupedFields` hook for it. + +* `` - renders a field icon. + +``` +const { isProcessing } = useExistingFieldsFetcher({ // this hook fetches fields info to understand which fields are empty. + dataViews: [currentDataView], + ... +}); + +const { fieldListFiltersProps, fieldListGroupedProps } = useGroupedFields({ + dataViewId: currentDataViewId, // pass `null` here for text-based queries to skip fields existence check + allFields, // pass `null` to show loading indicators + ... +}); + +// and now we can render a field list + + } +> + + +``` + +## Utils + +* `getFieldIconProps(field)` - gets icon's props to use with `` component + +* `getFieldIconType(field)` - gets icon's type for the field + +* `getFieldTypeName(field)` - gets a field type label to show to the user + +* `getFieldTypeDescription(field)` - gets a field type description to show to the user as help info + ## Public Services * `loadStats(...)` - returns the loaded field stats (can also work with Ad-hoc data views) @@ -63,41 +109,34 @@ These components can be combined and customized as the following: ## Hooks -* `useExistingFieldsFetcher(...)` - this hook is responsible for fetching fields existence info for specified data views. It can be used higher in components tree than `useExistingFieldsReader` hook. +* `useGroupedFields(...)` - this hook groups fields list into sections of Selected, Special, Available, Empty, Meta fields. + +* `useFieldFilters(...)` - manages state of `FieldListFilters` component. It is included into `useGroupedFields`. -* `useExistingFieldsReader(...)` - you can call this hook to read fields existence info which was fetched by `useExistingFieldsFetcher` hook. Using multiple "reader" hooks from different children components is supported. So you would need only one "fetcher" and as many "reader" hooks as necessary. +* `useQuerySubscriber(...)` - memorizes current query, filters and absolute date range which are set via UnifiedSearch. -* `useGroupedFields(...)` - this hook groups fields list into sections of Selected, Special, Available, Empty, Meta fields. +* `useExistingFieldsFetcher(...)` - this hook is responsible for fetching fields existence info for specified data views. It can be used higher in components tree than `useExistingFieldsReader` hook. + +* `useExistingFieldsReader(...)` - you can call this hook to read fields existence info which was fetched by `useExistingFieldsFetcher` hook. Using multiple "reader" hooks from different children components is supported. So you would need only one "fetcher" and as many "reader" hooks as necessary. It is included into `useGroupedFields`. -An example of using hooks together with ``: +An example of using hooks for fetching and reading info whether a field is empty or not: ``` +// `useQuerySubscriber` hook simplifies working with current query state which is required for `useExistingFieldsFetcher` +const querySubscriberResult = useQuerySubscriber(...); +// define a fetcher in any of your components const { refetchFieldsExistenceInfo, isProcessing } = useExistingFieldsFetcher({ dataViews, - query, - filters, - fromDate, - toDate, + query: querySubscriberResult.query, + filters: querySubscriberResult.filters, + fromDate: querySubscriberResult.fromDate, + toDate: querySubscriberResult.toDate, ... }); -const fieldsExistenceReader = useExistingFieldsReader() -const fieldListGroupedProps = useGroupedFields({ - dataViewId: currentDataViewId, // pass `null` here for text-based queries to skip fields existence check - allFields, // pass `null` to show loading indicators - fieldsExistenceReader, // pass `undefined` for text-based queries - ... -}); - -// and now we can render a field list - -// or check whether a field contains data +// define a reader in any of your components on the same page to check whether a field contains data const { hasFieldData } = useExistingFieldsReader(); -const hasData = hasFieldData(currentDataViewId, fieldName) // return a boolean +const hasData = hasFieldData(currentDataViewId, fieldName) // returns a boolean ``` ## Server APIs diff --git a/src/plugins/unified_field_list/public/components/field_icon/__snapshots__/field_icon.test.tsx.snap b/src/plugins/unified_field_list/public/components/field_icon/__snapshots__/field_icon.test.tsx.snap new file mode 100644 index 0000000000000..89bde8769a0c7 --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_icon/__snapshots__/field_icon.test.tsx.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UnifiedFieldList accepts additional props 1`] = ` + +`; + +exports[`UnifiedFieldList renders Document type properly 1`] = ` + +`; + +exports[`UnifiedFieldList renders Histogram type properly 1`] = ` + +`; + +exports[`UnifiedFieldList renders properly 1`] = ` + +`; + +exports[`UnifiedFieldList renders properly scripted fields 1`] = ` + +`; diff --git a/src/plugins/unified_field_list/public/components/field_icon/field_icon.test.tsx b/src/plugins/unified_field_list/public/components/field_icon/field_icon.test.tsx new file mode 100644 index 0000000000000..4f6e7ae06bfb8 --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_icon/field_icon.test.tsx @@ -0,0 +1,43 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import FieldIcon from './field_icon'; +import { getFieldIconProps } from './get_field_icon_props'; + +const dateField = dataView.getFieldByName('@timestamp')!; +const scriptedField = dataView.getFieldByName('script date')!; + +describe('UnifiedFieldList ', () => { + test('renders properly', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('renders properly scripted fields', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('accepts additional props', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('renders Document type properly', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('renders Histogram type properly', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/unified_field_list/public/components/field_icon/field_icon.tsx b/src/plugins/unified_field_list/public/components/field_icon/field_icon.tsx new file mode 100644 index 0000000000000..1485ebe1c8d02 --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_icon/field_icon.tsx @@ -0,0 +1,31 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { FieldIcon as KbnFieldIcon, FieldIconProps as KbnFieldIconProps } from '@kbn/react-field'; +import { getFieldTypeName } from '../../utils/field_types'; + +export type FieldIconProps = KbnFieldIconProps; + +const InnerFieldIcon: React.FC = ({ type, ...rest }) => { + return ; +}; + +export type GenericFieldIcon = typeof InnerFieldIcon; +const FieldIcon = React.memo(InnerFieldIcon) as GenericFieldIcon; + +// Necessary for React.lazy +// eslint-disable-next-line import/no-default-export +export default FieldIcon; + +function normalizeFieldType(type: string) { + if (type === 'histogram') { + return 'number'; + } + return type === 'document' ? 'number' : type; +} diff --git a/src/plugins/unified_field_list/public/components/field_icon/get_field_icon_props.ts b/src/plugins/unified_field_list/public/components/field_icon/get_field_icon_props.ts new file mode 100644 index 0000000000000..4ab52364abc66 --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_icon/get_field_icon_props.ts @@ -0,0 +1,21 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { type DataViewField } from '@kbn/data-views-plugin/common'; +import { FieldListItem } from '../../types'; +import { getFieldIconType } from '../../utils/field_types'; +import { type FieldIconProps } from './field_icon'; + +export function getFieldIconProps( + field: T +): FieldIconProps { + return { + type: getFieldIconType(field), + scripted: field.scripted, + }; +} diff --git a/src/plugins/unified_field_list/public/components/field_icon/index.tsx b/src/plugins/unified_field_list/public/components/field_icon/index.tsx new file mode 100644 index 0000000000000..590c4f488b43c --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_icon/index.tsx @@ -0,0 +1,28 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { Fragment } from 'react'; +import { type DataViewField } from '@kbn/data-views-plugin/common'; +import type { FieldIconProps, GenericFieldIcon } from './field_icon'; +import { type FieldListItem } from '../../types'; + +const Fallback = () => ; + +const LazyFieldIcon = React.lazy(() => import('./field_icon')) as GenericFieldIcon; + +function WrappedFieldIcon(props: FieldIconProps) { + return ( + }> + + + ); +} + +export const FieldIcon = WrappedFieldIcon; +export type { FieldIconProps }; +export { getFieldIconProps } from './get_field_icon_props'; diff --git a/src/plugins/unified_field_list/public/components/field_list/field_list.test.tsx b/src/plugins/unified_field_list/public/components/field_list/field_list.test.tsx new file mode 100644 index 0000000000000..c326f20d563b5 --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_list/field_list.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiText, EuiProgress } from '@elastic/eui'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { FieldList } from './field_list'; + +describe('UnifiedFieldList ', () => { + it('should render correctly when processing', async () => { + expect(mountWithIntl().find(EuiProgress)?.length).toBe(1); + expect(mountWithIntl().find(EuiProgress)?.length).toBe(0); + }); + + it('should render correctly with content', async () => { + const wrapper = mountWithIntl( + + {'content'} + + ); + + expect(wrapper.find(EuiText).first().text()).toBe('content'); + }); + + it('should render correctly with additional elements', async () => { + const wrapper = mountWithIntl( + {'prepend'}} + append={{'append'}} + > + {'content'} + + ); + + expect(wrapper.find(EuiText).first().text()).toBe('prepend'); + expect(wrapper.find(EuiText).at(1).text()).toBe('content'); + expect(wrapper.find(EuiText).at(2).text()).toBe('append'); + }); +}); diff --git a/src/plugins/unified_field_list/public/components/field_list/field_list.tsx b/src/plugins/unified_field_list/public/components/field_list/field_list.tsx new file mode 100644 index 0000000000000..9f51fd99e0ed4 --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_list/field_list.tsx @@ -0,0 +1,65 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; +import { css } from '@emotion/react'; + +const containerStyle = css` + position: relative; + width: 100%; + height: 100%; +`; + +/** + * A top level wrapper props + * @public + */ +export interface FieldListProps { + 'data-test-subj'?: string; + isProcessing: boolean; + prepend?: React.ReactNode; + append?: React.ReactNode; + className?: string; +} + +/** + * A top level wrapper for field list components (filters and field list groups) + * @param dataTestSubject + * @param isProcessing + * @param prepend + * @param append + * @param className + * @param children + * @public + * @constructor + */ +export const FieldList: React.FC = ({ + 'data-test-subj': dataTestSubject = 'fieldList', + isProcessing, + prepend, + append, + className, + children, +}) => { + return ( + + {isProcessing && } + {!!prepend && {prepend}} + {children} + {!!append && {append}} + + ); +}; diff --git a/src/plugins/unified_field_list/public/components/field_list/index.ts b/src/plugins/unified_field_list/public/components/field_list/index.ts new file mode 100644 index 0000000000000..a88be4dd650c4 --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_list/index.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { FieldList, type FieldListProps } from './field_list'; diff --git a/src/plugins/unified_field_list/public/components/field_list_filters/field_list_filters.tsx b/src/plugins/unified_field_list/public/components/field_list_filters/field_list_filters.tsx new file mode 100644 index 0000000000000..e1834dd8fd5f0 --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_list_filters/field_list_filters.tsx @@ -0,0 +1,87 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { type DataViewField } from '@kbn/data-views-plugin/common'; +import { FieldNameSearch, type FieldNameSearchProps } from './field_name_search'; +import { FieldTypeFilter, type FieldTypeFilterProps } from './field_type_filter'; +import { type FieldListItem } from '../../types'; + +/** + * Props for FieldListFilters component + */ +export interface FieldListFiltersProps { + 'data-test-subj'?: string; + docLinks: FieldTypeFilterProps['docLinks']; + selectedFieldTypes?: FieldTypeFilterProps['selectedFieldTypes']; + allFields?: FieldTypeFilterProps['allFields']; + getCustomFieldType?: FieldTypeFilterProps['getCustomFieldType']; + onSupportedFieldFilter?: FieldTypeFilterProps['onSupportedFieldFilter']; + onChangeFieldTypes: FieldTypeFilterProps['onChange']; + nameFilter: FieldNameSearchProps['nameFilter']; + screenReaderDescriptionId?: FieldNameSearchProps['screenReaderDescriptionId']; + onChangeNameFilter: FieldNameSearchProps['onChange']; +} + +/** + * Field list filters which include search by field name and filtering by field type. + * Use in combination with `useGroupedFields` hook. Or for more control - `useFieldFilters()` hook. + * @param dataTestSubject + * @param docLinks + * @param selectedFieldTypes + * @param allFields + * @param getCustomFieldType + * @param onSupportedFieldFilter + * @param onChangeFieldTypes + * @param nameFilter + * @param screenReaderDescriptionId + * @param onChangeNameFilter + * @public + * @constructor + */ +function InnerFieldListFilters({ + 'data-test-subj': dataTestSubject = 'fieldListFilters', + docLinks, + selectedFieldTypes, + allFields, + getCustomFieldType, + onSupportedFieldFilter, + onChangeFieldTypes, + nameFilter, + screenReaderDescriptionId, + onChangeNameFilter, +}: FieldListFiltersProps) { + return ( + + ) : undefined + } + nameFilter={nameFilter} + screenReaderDescriptionId={screenReaderDescriptionId} + onChange={onChangeNameFilter} + /> + ); +} + +export type GenericFieldListFilters = typeof InnerFieldListFilters; +const FieldListFilters = React.memo(InnerFieldListFilters) as GenericFieldListFilters; + +// Necessary for React.lazy +// eslint-disable-next-line import/no-default-export +export default FieldListFilters; diff --git a/src/plugins/unified_field_list/public/components/field_list_filters/field_name_search.test.tsx b/src/plugins/unified_field_list/public/components/field_list_filters/field_name_search.test.tsx new file mode 100644 index 0000000000000..12780a111fae0 --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_list_filters/field_name_search.test.tsx @@ -0,0 +1,53 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { act } from 'react-dom/test-utils'; +import { FieldNameSearch, type FieldNameSearchProps } from './field_name_search'; + +describe('UnifiedFieldList ', () => { + it('should render correctly', async () => { + const props: FieldNameSearchProps = { + nameFilter: '', + onChange: jest.fn(), + screenReaderDescriptionId: 'htmlId', + 'data-test-subj': 'searchInput', + }; + const wrapper = mountWithIntl(); + expect(wrapper.find('input').prop('aria-describedby')).toBe('htmlId'); + + act(() => { + wrapper.find('input').simulate('change', { + target: { value: 'hi' }, + }); + }); + + expect(props.onChange).toBeCalledWith('hi'); + }); + + it('should update correctly', async () => { + const props: FieldNameSearchProps = { + nameFilter: 'this', + onChange: jest.fn(), + screenReaderDescriptionId: 'htmlId', + 'data-test-subj': 'searchInput', + }; + const wrapper = mountWithIntl(); + + expect(wrapper.find('input').prop('value')).toBe('this'); + + wrapper.setProps({ + nameFilter: 'that', + }); + + expect(wrapper.find('input').prop('value')).toBe('that'); + + expect(props.onChange).not.toBeCalled(); + }); +}); diff --git a/src/plugins/unified_field_list/public/components/field_list_filters/field_name_search.tsx b/src/plugins/unified_field_list/public/components/field_list_filters/field_name_search.tsx new file mode 100644 index 0000000000000..91d78850e4453 --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_list_filters/field_name_search.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFieldSearch, type EuiFieldSearchProps } from '@elastic/eui'; + +/** + * Props for FieldNameSearch component + */ +export interface FieldNameSearchProps { + 'data-test-subj': string; + append?: EuiFieldSearchProps['append']; + nameFilter: string; + screenReaderDescriptionId?: string; + onChange: (nameFilter: string) => unknown; +} + +/** + * Search input for fields list + * @param dataTestSubject + * @param append + * @param nameFilter + * @param screenReaderDescriptionId + * @param onChange + * @constructor + */ +export const FieldNameSearch: React.FC = ({ + 'data-test-subj': dataTestSubject, + append, + nameFilter, + screenReaderDescriptionId, + onChange, +}) => { + const searchPlaceholder = i18n.translate('unifiedFieldList.fieldNameSearch.filterByNameLabel', { + defaultMessage: 'Search field names', + description: 'Search the list of fields in the data view for the provided text', + }); + + return ( + onChange(event.target.value)} + placeholder={searchPlaceholder} + value={nameFilter} + append={append} + /> + ); +}; diff --git a/src/plugins/unified_field_list/public/components/field_list_filters/field_type_filter.test.tsx b/src/plugins/unified_field_list/public/components/field_list_filters/field_type_filter.test.tsx new file mode 100644 index 0000000000000..01bc1c4147f15 --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_list_filters/field_type_filter.test.tsx @@ -0,0 +1,138 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { EuiContextMenuItem } from '@elastic/eui'; +import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { coreMock } from '@kbn/core/public/mocks'; +import { type DataViewField } from '@kbn/data-views-plugin/common'; +import { FieldTypeFilter, type FieldTypeFilterProps } from './field_type_filter'; + +const docLinks = coreMock.createStart().docLinks; + +describe('UnifiedFieldList ', () => { + async function openPopover(wrapper: ReactWrapper, props: FieldTypeFilterProps) { + act(() => { + wrapper + .find(`[data-test-subj="${props['data-test-subj']}FieldTypeFilterToggle"]`) + .last() + .simulate('click'); + }); + + // wait for lazy modules if any + await new Promise((resolve) => setTimeout(resolve, 0)); + await wrapper.update(); + } + + async function toggleType(wrapper: ReactWrapper, fieldType: string) { + act(() => { + wrapper.find(`[data-test-subj="typeFilter-${fieldType}"]`).first().simulate('click'); + }); + + await wrapper.update(); + } + + function findClearAllButton(wrapper: ReactWrapper, props: FieldTypeFilterProps) { + return wrapper.find(`[data-test-subj="${props['data-test-subj']}FieldTypeFilterClearAll"]`); + } + + it("should render correctly and don't calculate counts unless opened", async () => { + const props: FieldTypeFilterProps = { + selectedFieldTypes: [], + allFields: dataView.fields, + docLinks, + 'data-test-subj': 'filters', + getCustomFieldType: jest.fn((field) => field.type), + onChange: jest.fn(), + }; + const wrapper = await mountWithIntl(); + expect(wrapper.find(EuiContextMenuItem)?.length).toBe(0); + expect(props.getCustomFieldType).not.toBeCalled(); + + await openPopover(wrapper, props); + + expect(wrapper.find(EuiContextMenuItem)?.length).toBe(11); + expect( + wrapper + .find(EuiContextMenuItem) + .map((item) => item.text()) + .join(', ') + ).toBe( + // format:type_icon type_name help_icon count + 'BooleanBooleanInfo1, ConflictConflictInfo1, DateDateInfo4, Geo pointGeo pointInfo2, Geo shapeGeo shapeInfo1, IP addressIP addressInfo1, KeywordKeywordInfo4, Murmur3Murmur3Info2, NumberNumberInfo3, StringStringInfo1, TextTextInfo5' + ); + expect(props.getCustomFieldType).toHaveBeenCalledTimes(props.allFields?.length ?? 0); + expect(props.onChange).not.toBeCalled(); + expect(findClearAllButton(wrapper, props)?.length).toBe(0); + }); + + it('should exclude custom unsupported fields', async () => { + const props: FieldTypeFilterProps = { + selectedFieldTypes: [], + allFields: dataView.fields, + docLinks, + 'data-test-subj': 'filters', + onSupportedFieldFilter: (field) => ['number', 'date'].includes(field.type), + onChange: jest.fn(), + }; + const wrapper = await mountWithIntl(); + expect(wrapper.find(EuiContextMenuItem)?.length).toBe(0); + + await openPopover(wrapper, props); + + expect(wrapper.find(EuiContextMenuItem)?.length).toBe(2); + expect( + wrapper + .find(EuiContextMenuItem) + .map((item) => item.text()) + .join(', ') + ).toBe('DateDateInfo4, NumberNumberInfo3'); + }); + + it('should select items correctly', async () => { + const props: FieldTypeFilterProps = { + selectedFieldTypes: ['date', 'number'], + allFields: dataView.fields, + docLinks, + 'data-test-subj': 'filters', + onChange: jest.fn(), + }; + const wrapper = await mountWithIntl(); + expect(wrapper.find(EuiContextMenuItem)?.length).toBe(0); + + await openPopover(wrapper, props); + + const clearAllButton = findClearAllButton(wrapper, props)?.first(); + expect(wrapper.find(EuiContextMenuItem)?.length).toBe(11); + expect(clearAllButton?.length).toBe(1); + expect( + wrapper + .find(EuiContextMenuItem) + .map((item) => `${item.prop('icon')}-${item.text()}`) + .join(', ') + ).toBe( + // format:selection_icon type_icon type_name help_icon count + 'empty-BooleanBooleanInfo1, empty-ConflictConflictInfo1, check-DateDateInfo4, empty-Geo pointGeo pointInfo2, empty-Geo shapeGeo shapeInfo1, empty-IP addressIP addressInfo1, empty-KeywordKeywordInfo4, empty-Murmur3Murmur3Info2, check-NumberNumberInfo3, empty-StringStringInfo1, empty-TextTextInfo5' + ); + + await toggleType(wrapper, 'boolean'); + + expect(props.onChange).toHaveBeenCalledWith(['date', 'number', 'boolean']); + + await toggleType(wrapper, 'date'); + + expect(props.onChange).toHaveBeenNthCalledWith(2, ['number']); + + clearAllButton.simulate('click'); + + expect(props.onChange).toHaveBeenNthCalledWith(3, []); + }); +}); diff --git a/src/plugins/unified_field_list/public/components/field_list_filters/field_type_filter.tsx b/src/plugins/unified_field_list/public/components/field_list_filters/field_type_filter.tsx new file mode 100644 index 0000000000000..db98acad65013 --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_list_filters/field_type_filter.tsx @@ -0,0 +1,273 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { css } from '@emotion/react'; +import { + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiFilterButton, + EuiIcon, + EuiLoadingSpinner, + EuiPopover, + EuiPanel, + EuiText, + EuiLink, + EuiPopoverTitle, + EuiPopoverFooter, + EuiNotificationBadge, + EuiIconTip, + EuiButtonEmpty, + useEuiTheme, + EuiTitle, +} from '@elastic/eui'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { type DataViewField } from '@kbn/data-views-plugin/common'; +import { FieldIcon } from '../field_icon'; +import { + getFieldIconType, + getFieldTypeName, + getFieldTypeDescription, + isKnownFieldType, + KNOWN_FIELD_TYPE_LIST, +} from '../../utils/field_types'; +import type { FieldListItem, FieldTypeKnown, GetCustomFieldType } from '../../types'; + +const EQUAL_HEIGHT_OFFSET = 2; // to avoid changes in the header's height after "Clear all" button appears +const popoverTitleStyle = css` + padding: ${EQUAL_HEIGHT_OFFSET}px 0; +`; +const filterButtonStyle = css` + .euiFilterButton__textShift { + min-width: 0; + line-height: 1; + } +`; + +/** + * Props for FieldTypeFilter component + */ +export interface FieldTypeFilterProps { + 'data-test-subj': string; + docLinks: CoreStart['docLinks']; + allFields: T[] | null; + getCustomFieldType?: GetCustomFieldType; + selectedFieldTypes: FieldTypeKnown[]; + onSupportedFieldFilter?: (field: T) => boolean; + onChange: (fieldTypes: FieldTypeKnown[]) => unknown; +} + +/** + * A popover with field type filters + * @param dataTestSubject + * @param docLinks + * @param allFields + * @param getCustomFieldType + * @param selectedFieldTypes + * @param onSupportedFieldFilter + * @param onChange + * @constructor + */ +export function FieldTypeFilter({ + 'data-test-subj': dataTestSubject, + docLinks, + allFields, + getCustomFieldType, + selectedFieldTypes, + onSupportedFieldFilter, + onChange, +}: FieldTypeFilterProps) { + const testSubj = `${dataTestSubject}FieldTypeFilter`; + const [isOpen, setIsOpen] = useState(false); + const [typeCounts, setTypeCounts] = useState>(); + + const { euiTheme } = useEuiTheme(); + + const titleStyle = useMemo( + () => css` + padding-top: calc(${euiTheme.size.m} - ${EQUAL_HEIGHT_OFFSET}px); + padding-bottom: calc(${euiTheme.size.m} - ${EQUAL_HEIGHT_OFFSET}px); + padding-left: ${euiTheme.size.m}; + padding-right: ${euiTheme.size.xs}; + `, + [euiTheme.size.m, euiTheme.size.xs] + ); + + const itemStyle = useMemo( + () => css` + font-size: ${euiTheme.size.m}; + padding: ${euiTheme.size.s} ${euiTheme.size.m}; + + & + & { + border-top: 1px solid ${euiTheme.colors.lightestShade}; + } + `, + [euiTheme] + ); + + useEffect(() => { + // calculate counts only if user opened the popover + if (!isOpen || !allFields?.length) { + setTypeCounts(undefined); + return; + } + const counts = new Map(); + allFields.forEach((field) => { + if (onSupportedFieldFilter && !onSupportedFieldFilter(field)) { + return; + } + const type = getFieldIconType(field, getCustomFieldType); + if (isKnownFieldType(type)) { + counts.set(type, (counts.get(type) || 0) + 1); + } + }); + setTypeCounts(counts); + }, [isOpen, allFields, setTypeCounts, getCustomFieldType, onSupportedFieldFilter]); + + const availableFieldTypes = useMemo(() => { + // sorting is defined by items in KNOWN_FIELD_TYPE_LIST + return KNOWN_FIELD_TYPE_LIST.filter((type) => { + const knownTypeCount = typeCounts?.get(type) ?? 0; + // always include current field type filters - there may not be any fields of the type of an existing type filter on data view switch, but we still need to include the existing filter in the list so that the user can remove it + return knownTypeCount > 0 || selectedFieldTypes.includes(type); + }); + }, [typeCounts, selectedFieldTypes]); + + const clearAll = useCallback(() => { + onChange([]); + }, [onChange]); + + return ( + setIsOpen(false)} + button={ + setIsOpen((value) => !value)} + > + + + } + > + <> + + + + +
    + {i18n.translate('unifiedFieldList.fieldTypeFilter.title', { + defaultMessage: 'Filter by field type', + })} +
    +
    +
    + {selectedFieldTypes.length > 0 && ( + + + {i18n.translate('unifiedFieldList.fieldTypeFilter.clearAllLink', { + defaultMessage: 'Clear all', + })} + + + )} +
    +
    + {availableFieldTypes.length > 0 ? ( + ( + { + onChange( + selectedFieldTypes.includes(type) + ? selectedFieldTypes.filter((t) => t !== type) + : [...selectedFieldTypes, type] + ); + }} + > + + + + + + + + {getFieldTypeName(type)} + + + + + + + + + {typeCounts?.get(type) ?? 0} + + + + + ))} + /> + ) : ( + + + + + + + + )} + + + +

    + {i18n.translate('unifiedFieldList.fieldTypeFilter.learnMoreText', { + defaultMessage: 'Learn more about', + })} +   + + + +

    +
    +
    +
    + +
    + ); +} diff --git a/src/plugins/unified_field_list/public/components/field_list_filters/index.tsx b/src/plugins/unified_field_list/public/components/field_list_filters/index.tsx new file mode 100644 index 0000000000000..b9572a19427cb --- /dev/null +++ b/src/plugins/unified_field_list/public/components/field_list_filters/index.tsx @@ -0,0 +1,31 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { Fragment } from 'react'; +import { type DataViewField } from '@kbn/data-views-plugin/common'; +import type { FieldListFiltersProps, GenericFieldListFilters } from './field_list_filters'; +import { type FieldListItem } from '../../types'; + +const Fallback = () => ; + +const LazyFieldListFilters = React.lazy( + () => import('./field_list_filters') +) as GenericFieldListFilters; + +function WrappedFieldListFilters( + props: FieldListFiltersProps +) { + return ( + }> + {...props} /> + + ); +} + +export const FieldListFilters = WrappedFieldListFilters; +export type { FieldListFiltersProps }; diff --git a/src/plugins/unified_field_list/public/components/field_list/field_list_grouped.scss b/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.scss similarity index 100% rename from src/plugins/unified_field_list/public/components/field_list/field_list_grouped.scss rename to src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.scss diff --git a/src/plugins/unified_field_list/public/components/field_list/field_list_grouped.test.tsx b/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.test.tsx similarity index 80% rename from src/plugins/unified_field_list/public/components/field_list/field_list_grouped.test.tsx rename to src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.test.tsx index 4d1cb45fe1936..778f38168e6c1 100644 --- a/src/plugins/unified_field_list/public/components/field_list/field_list_grouped.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.test.tsx @@ -14,6 +14,7 @@ import { mountWithIntl } from '@kbn/test-jest-helpers'; import { DataViewField } from '@kbn/data-views-plugin/common'; import { ReactWrapper } from 'enzyme'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { coreMock } from '@kbn/core/public/mocks'; import FieldListGrouped, { type FieldListGroupedProps } from './field_list_grouped'; import { ExistenceFetchStatus } from '../../types'; import { FieldsAccordion } from './fields_accordion'; @@ -35,6 +36,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { const dataViews = dataViewPluginMocks.createStartContract(); mockedServices = { dataViews, + core: coreMock.createStart(), }; dataViews.get.mockImplementation(async (id: string) => { @@ -46,7 +48,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { fieldsExistenceStatus: ExistenceFetchStatus.succeeded, scrollToTopResetCounter: 0, fieldsExistInIndex: true, - screenReaderDescriptionForSearchInputId: 'testId', + screenReaderDescriptionId: 'testId', renderFieldItem: jest.fn(({ field, itemIndex, groupIndex }) => ( + useGroupedFields()', () => { async function mountGroupedList({ listProps, hookParams }: WrapperProps): Promise { const Wrapper: React.FC = (props) => { - const { fieldGroups } = useGroupedFields({ + const { + fieldListGroupedProps: { fieldGroups }, + } = useGroupedFields({ ...props.hookParams, services: mockedServices, }); @@ -93,9 +97,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { /> ); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe(''); + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe(''); }); it('renders correctly in loading state', async () => { @@ -113,9 +115,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { expect(wrapper.find(FieldListGrouped).prop('fieldsExistenceStatus')).toBe( ExistenceFetchStatus.unknown ); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe(''); + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe(''); expect(wrapper.find(FieldsAccordion)).toHaveLength(3); expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(3); expect( @@ -136,9 +136,9 @@ describe('UnifiedFieldList + useGroupedFields()', () => { expect(wrapper.find(FieldListGrouped).prop('fieldsExistenceStatus')).toBe( ExistenceFetchStatus.succeeded ); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('25 available fields. 0 empty fields. 3 meta fields.'); + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( + '25 available fields. 0 empty fields. 3 meta fields.' + ); expect(wrapper.find(FieldsAccordion)).toHaveLength(3); expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); expect( @@ -165,9 +165,9 @@ describe('UnifiedFieldList + useGroupedFields()', () => { expect(wrapper.find(FieldListGrouped).prop('fieldsExistenceStatus')).toBe( ExistenceFetchStatus.failed ); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('25 available fields. 0 empty fields. 3 meta fields.'); + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( + '25 available fields. 0 empty fields. 3 meta fields.' + ); expect(wrapper.find(FieldsAccordion)).toHaveLength(3); expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); expect( @@ -191,9 +191,9 @@ describe('UnifiedFieldList + useGroupedFields()', () => { }, }); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('0 available fields. 0 empty fields. 0 meta fields.'); + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( + '0 available fields. 0 empty fields. 0 meta fields.' + ); expect(wrapper.find(FieldsAccordion)).toHaveLength(3); expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); expect( @@ -214,9 +214,9 @@ describe('UnifiedFieldList + useGroupedFields()', () => { }, }); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('1 selected field. 28 available fields.'); + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( + '1 selected field. 28 available fields.' + ); expect( wrapper.find(FieldsAccordion).map((accordion) => accordion.prop('paginatedFields').length) ).toStrictEqual([1, 28]); @@ -234,9 +234,9 @@ describe('UnifiedFieldList + useGroupedFields()', () => { }, }); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('25 available fields. 0 empty fields. 3 meta fields.'); + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( + '25 available fields. 0 empty fields. 3 meta fields.' + ); expect( wrapper.find(FieldsAccordion).map((accordion) => accordion.prop('paginatedFields').length) ).toStrictEqual([25, 0, 0]); @@ -267,9 +267,9 @@ describe('UnifiedFieldList + useGroupedFields()', () => { }, }); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.'); + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( + '25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.' + ); expect( wrapper.find(FieldsAccordion).map((accordion) => accordion.prop('paginatedFields').length) ).toStrictEqual([25, 0, 0, 0]); @@ -314,9 +314,9 @@ describe('UnifiedFieldList + useGroupedFields()', () => { hookParams, }); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.'); + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( + '25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.' + ); await act(async () => { await wrapper.setProps({ @@ -328,9 +328,9 @@ describe('UnifiedFieldList + useGroupedFields()', () => { await wrapper.update(); }); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('2 available fields. 8 unmapped fields. 0 empty fields. 0 meta fields.'); + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( + '2 available fields. 8 unmapped fields. 0 empty fields. 0 meta fields.' + ); await act(async () => { await wrapper.setProps({ @@ -342,9 +342,9 @@ describe('UnifiedFieldList + useGroupedFields()', () => { await wrapper.update(); }); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('0 available fields. 12 unmapped fields. 0 empty fields. 3 meta fields.'); + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( + '0 available fields. 12 unmapped fields. 0 empty fields. 3 meta fields.' + ); }); it('renders correctly when non-supported fields are filtered out', async () => { @@ -360,9 +360,9 @@ describe('UnifiedFieldList + useGroupedFields()', () => { hookParams, }); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.'); + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( + '25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.' + ); await act(async () => { await wrapper.setProps({ @@ -374,9 +374,9 @@ describe('UnifiedFieldList + useGroupedFields()', () => { await wrapper.update(); }); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('23 available fields. 104 unmapped fields. 0 empty fields. 3 meta fields.'); + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( + '23 available fields. 104 unmapped fields. 0 empty fields. 3 meta fields.' + ); }); it('renders correctly when selected fields are present', async () => { @@ -392,9 +392,9 @@ describe('UnifiedFieldList + useGroupedFields()', () => { hookParams, }); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe('25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.'); + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( + '25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.' + ); await act(async () => { await wrapper.setProps({ @@ -407,9 +407,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { await wrapper.update(); }); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe( + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( '2 selected fields. 25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.' ); }); @@ -429,9 +427,7 @@ describe('UnifiedFieldList + useGroupedFields()', () => { hookParams, }); - expect( - wrapper.find(`#${defaultProps.screenReaderDescriptionForSearchInputId}`).first().text() - ).toBe( + expect(wrapper.find(`#${defaultProps.screenReaderDescriptionId}`).first().text()).toBe( '2 selected fields. 10 popular fields. 25 available fields. 112 unmapped fields. 0 empty fields. 3 meta fields.' ); }); diff --git a/src/plugins/unified_field_list/public/components/field_list/field_list_grouped.tsx b/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.tsx similarity index 97% rename from src/plugins/unified_field_list/public/components/field_list/field_list_grouped.tsx rename to src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.tsx index 94a76d9b6a6dc..9e81cb8c5d476 100644 --- a/src/plugins/unified_field_list/public/components/field_list/field_list_grouped.tsx +++ b/src/plugins/unified_field_list/public/components/field_list_grouped/field_list_grouped.tsx @@ -34,7 +34,7 @@ export interface FieldListGroupedProps { fieldsExistInIndex: boolean; renderFieldItem: FieldsAccordionProps['renderFieldItem']; scrollToTopResetCounter: number; - screenReaderDescriptionForSearchInputId?: string; + screenReaderDescriptionId?: string; 'data-test-subj'?: string; } @@ -44,7 +44,7 @@ function InnerFieldListGrouped({ fieldsExistInIndex, renderFieldItem, scrollToTopResetCounter, - screenReaderDescriptionForSearchInputId, + screenReaderDescriptionId, 'data-test-subj': dataTestSubject = 'fieldListGrouped', }: FieldListGroupedProps) { const hasSyncedExistingFields = @@ -118,11 +118,11 @@ function InnerFieldListGrouped({ onScroll={throttle(lazyScroll, 100)} >
    - {Boolean(screenReaderDescriptionForSearchInputId) && ( + {Boolean(screenReaderDescriptionId) && (
    {hasSyncedExistingFields @@ -203,7 +203,7 @@ function InnerFieldListGrouped({ {hasSpecialFields && ( <>
      - {fieldGroupsToCollapse.flatMap(([key, { fields }]) => + {fieldGroupsToCollapse.flatMap(([key, { fields, fieldSearchHighlight }]) => fields.map((field, index) => ( {renderFieldItem({ @@ -212,6 +212,7 @@ function InnerFieldListGrouped({ groupIndex: 0, groupName: key as FieldsGroupNames, hideDetails: true, + fieldSearchHighlight, })} )) @@ -236,6 +237,7 @@ function InnerFieldListGrouped({ hasLoaded={hasSyncedExistingFields} fieldsCount={fieldGroup.fields.length} isFiltered={fieldGroup.fieldCount !== fieldGroup.fields.length} + fieldSearchHighlight={fieldGroup.fieldSearchHighlight} paginatedFields={paginatedFields[key]} groupIndex={index + 1} groupName={key as FieldsGroupNames} diff --git a/src/plugins/unified_field_list/public/components/field_list/fields_accordion.scss b/src/plugins/unified_field_list/public/components/field_list_grouped/fields_accordion.scss similarity index 100% rename from src/plugins/unified_field_list/public/components/field_list/fields_accordion.scss rename to src/plugins/unified_field_list/public/components/field_list_grouped/fields_accordion.scss diff --git a/src/plugins/unified_field_list/public/components/field_list/fields_accordion.test.tsx b/src/plugins/unified_field_list/public/components/field_list_grouped/fields_accordion.test.tsx similarity index 78% rename from src/plugins/unified_field_list/public/components/field_list/fields_accordion.test.tsx rename to src/plugins/unified_field_list/public/components/field_list_grouped/fields_accordion.test.tsx index 6c94f8a8e8335..09f0e3352a915 100644 --- a/src/plugins/unified_field_list/public/components/field_list/fields_accordion.test.tsx +++ b/src/plugins/unified_field_list/public/components/field_list_grouped/fields_accordion.test.tsx @@ -30,7 +30,11 @@ describe('UnifiedFieldList ', () => { isFiltered: false, paginatedFields, renderCallout: () =>
      Callout
      , - renderFieldItem: ({ field }) => {field.name}, + renderFieldItem: ({ field, fieldSearchHighlight }) => ( + + {field.name} + + ), }; }); @@ -60,4 +64,14 @@ describe('UnifiedFieldList ', () => { const wrapper = mountWithIntl(); expect(wrapper.find(EuiLoadingSpinner).length).toEqual(1); }); + + it('renders items with the provided highlight', () => { + const wrapperWithHighlight = mountWithIntl( + + ); + expect(wrapperWithHighlight.find(EuiText).last().prop('data-highlight')).toBe('test-highlight'); + + const wrapperWithoutHighlight = mountWithIntl(); + expect(wrapperWithoutHighlight.find(EuiText).last().prop('data-highlight')).toBeUndefined(); + }); }); diff --git a/src/plugins/unified_field_list/public/components/field_list/fields_accordion.tsx b/src/plugins/unified_field_list/public/components/field_list_grouped/fields_accordion.tsx similarity index 93% rename from src/plugins/unified_field_list/public/components/field_list/fields_accordion.tsx rename to src/plugins/unified_field_list/public/components/field_list_grouped/fields_accordion.tsx index 8b7ca22bff676..cda13fd18a42e 100644 --- a/src/plugins/unified_field_list/public/components/field_list/fields_accordion.tsx +++ b/src/plugins/unified_field_list/public/components/field_list_grouped/fields_accordion.tsx @@ -33,6 +33,7 @@ export interface FieldsAccordionProps { isFiltered: boolean; groupIndex: number; groupName: FieldsGroupNames; + fieldSearchHighlight?: string; paginatedFields: T[]; renderFieldItem: (params: { field: T; @@ -40,6 +41,7 @@ export interface FieldsAccordionProps { itemIndex: number; groupIndex: number; groupName: FieldsGroupNames; + fieldSearchHighlight?: string; }) => JSX.Element; renderCallout: () => JSX.Element; showExistenceFetchError?: boolean; @@ -58,6 +60,7 @@ function InnerFieldsAccordion({ isFiltered, groupIndex, groupName, + fieldSearchHighlight, paginatedFields, renderFieldItem, renderCallout, @@ -153,7 +156,14 @@ function InnerFieldsAccordion({ {paginatedFields && paginatedFields.map((field, index) => ( - {renderFieldItem({ field, itemIndex: index, groupIndex, groupName, hideDetails })} + {renderFieldItem({ + field, + itemIndex: index, + groupIndex, + groupName, + hideDetails, + fieldSearchHighlight, + })} ))}
    diff --git a/src/plugins/unified_field_list/public/components/field_list/index.tsx b/src/plugins/unified_field_list/public/components/field_list_grouped/index.tsx similarity index 100% rename from src/plugins/unified_field_list/public/components/field_list/index.tsx rename to src/plugins/unified_field_list/public/components/field_list_grouped/index.tsx diff --git a/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.test.tsx b/src/plugins/unified_field_list/public/components/field_list_grouped/no_fields_callout.test.tsx similarity index 100% rename from src/plugins/unified_field_list/public/components/field_list/no_fields_callout.test.tsx rename to src/plugins/unified_field_list/public/components/field_list_grouped/no_fields_callout.test.tsx diff --git a/src/plugins/unified_field_list/public/components/field_list/no_fields_callout.tsx b/src/plugins/unified_field_list/public/components/field_list_grouped/no_fields_callout.tsx similarity index 100% rename from src/plugins/unified_field_list/public/components/field_list/no_fields_callout.tsx rename to src/plugins/unified_field_list/public/components/field_list_grouped/no_fields_callout.tsx diff --git a/src/plugins/unified_field_list/public/hooks/use_existing_fields.ts b/src/plugins/unified_field_list/public/hooks/use_existing_fields.ts index 583ca32ce7508..6b523d155393c 100644 --- a/src/plugins/unified_field_list/public/hooks/use_existing_fields.ts +++ b/src/plugins/unified_field_list/public/hooks/use_existing_fields.ts @@ -68,6 +68,12 @@ const unknownInfo: ExistingFieldsInfo = { const globalMap$ = new BehaviorSubject(initialData); // for syncing between hooks let lastFetchId: string = ''; // persist last fetch id to skip older requests/responses if any +/** + * Fetches info whether a field contains data or it's empty. + * Can be used in combination with `useQuerySubscriber` hook for gathering the required params. + * @param params + * @public + */ export const useExistingFieldsFetcher = ( params: ExistingFieldsFetcherParams ): ExistingFieldsFetcher => { diff --git a/src/plugins/unified_field_list/public/hooks/use_field_filters.test.tsx b/src/plugins/unified_field_list/public/hooks/use_field_filters.test.tsx new file mode 100644 index 0000000000000..77327fcbee402 --- /dev/null +++ b/src/plugins/unified_field_list/public/hooks/use_field_filters.test.tsx @@ -0,0 +1,113 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { act } from 'react-test-renderer'; +import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { type DataViewField } from '@kbn/data-plugin/common'; +import { coreMock } from '@kbn/core/public/mocks'; +import { useFieldFilters, type FieldFiltersParams } from './use_field_filters'; + +describe('UnifiedFieldList useFieldFilters()', () => { + let mockedServices: FieldFiltersParams['services']; + + beforeEach(() => { + const core = coreMock.createStart(); + mockedServices = { + core, + }; + }); + + it('should work correctly for no filters', async () => { + const props: FieldFiltersParams = { + allFields: dataView.fields, + services: mockedServices, + }; + const { result } = renderHook(useFieldFilters, { + initialProps: props, + }); + + expect(result.current.fieldSearchHighlight).toBe(''); + expect(result.current.onFilterField).toBeUndefined(); + expect(result.current.fieldListFiltersProps.getCustomFieldType).toBeUndefined(); + expect(result.current.fieldListFiltersProps).toStrictEqual( + expect.objectContaining({ + docLinks: mockedServices.core.docLinks, + allFields: props.allFields, + nameFilter: '', + selectedFieldTypes: [], + }) + ); + }); + + it('should update correctly on search by name', async () => { + const props: FieldFiltersParams = { + allFields: dataView.fields, + services: mockedServices, + }; + const { result } = renderHook(useFieldFilters, { + initialProps: props, + }); + + expect(result.current.fieldSearchHighlight).toBe(''); + expect(result.current.onFilterField).toBeUndefined(); + + act(() => { + result.current.fieldListFiltersProps.onChangeNameFilter('Time'); + }); + + expect(result.current.fieldSearchHighlight).toBe('time'); + expect(result.current.onFilterField).toBeDefined(); + expect(result.current.onFilterField!({ displayName: 'time test' } as DataViewField)).toBe(true); + expect(result.current.onFilterField!(dataView.getFieldByName('@timestamp')!)).toBe(true); + expect(result.current.onFilterField!(dataView.getFieldByName('bytes')!)).toBe(false); + }); + + it('should update correctly on filter by type', async () => { + const props: FieldFiltersParams = { + allFields: dataView.fields, + services: mockedServices, + }; + const { result } = renderHook(useFieldFilters, { + initialProps: props, + }); + + expect(result.current.onFilterField).toBeUndefined(); + + act(() => { + result.current.fieldListFiltersProps.onChangeFieldTypes(['number']); + }); + + expect(result.current.onFilterField).toBeDefined(); + expect(result.current.onFilterField!(dataView.getFieldByName('@timestamp')!)).toBe(false); + expect(result.current.onFilterField!(dataView.getFieldByName('bytes')!)).toBe(true); + }); + + it('should update correctly on filter by custom field type', async () => { + const props: FieldFiltersParams = { + allFields: dataView.fields, + services: mockedServices, + getCustomFieldType: (field) => field.name, + }; + const { result } = renderHook(useFieldFilters, { + initialProps: props, + }); + + expect(result.current.onFilterField).toBeUndefined(); + expect(result.current.fieldListFiltersProps.getCustomFieldType).toBe(props.getCustomFieldType); + + act(() => { + result.current.fieldListFiltersProps.onChangeFieldTypes(['bytes', '@timestamp']); + }); + + expect(result.current.onFilterField).toBeDefined(); + expect(result.current.onFilterField!(dataView.getFieldByName('@timestamp')!)).toBe(true); + expect(result.current.onFilterField!(dataView.getFieldByName('bytes')!)).toBe(true); + expect(result.current.onFilterField!(dataView.getFieldByName('extension')!)).toBe(false); + }); +}); diff --git a/src/plugins/unified_field_list/public/hooks/use_field_filters.ts b/src/plugins/unified_field_list/public/hooks/use_field_filters.ts new file mode 100644 index 0000000000000..803739caba7c6 --- /dev/null +++ b/src/plugins/unified_field_list/public/hooks/use_field_filters.ts @@ -0,0 +1,102 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useMemo, useState } from 'react'; +import { htmlIdGenerator } from '@elastic/eui'; +import { type DataViewField } from '@kbn/data-views-plugin/common'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import { type FieldListFiltersProps } from '../components/field_list_filters'; +import { type FieldListItem, type FieldTypeKnown, GetCustomFieldType } from '../types'; +import { getFieldIconType } from '../utils/field_types'; + +const htmlId = htmlIdGenerator('fieldList'); + +/** + * Input params for useFieldFilters hook + */ +export interface FieldFiltersParams { + allFields: T[] | null; + getCustomFieldType?: GetCustomFieldType; + onSupportedFieldFilter?: (field: T) => boolean; + services: { + core: Pick; + }; +} + +/** + * Output of useFieldFilters hook + */ +export interface FieldFiltersResult { + fieldSearchHighlight: string; + fieldListFiltersProps: FieldListFiltersProps; + onFilterField?: (field: T) => boolean; +} + +/** + * A hook for managing field search and filters state + * @param allFields + * @param getCustomFieldType + * @param onSupportedFieldFilter + * @param services + * @public + */ +export function useFieldFilters({ + allFields, + getCustomFieldType, + onSupportedFieldFilter, + services, +}: FieldFiltersParams): FieldFiltersResult { + const [selectedFieldTypes, setSelectedFieldTypes] = useState([]); + const [nameFilter, setNameFilter] = useState(''); + const screenReaderDescriptionId = useMemo(() => htmlId(), []); + const docLinks = services.core.docLinks; + + return useMemo(() => { + const fieldSearchHighlight = nameFilter.toLowerCase(); + return { + fieldSearchHighlight, + fieldListFiltersProps: { + docLinks, + selectedFieldTypes, + allFields, + getCustomFieldType, + onSupportedFieldFilter, + onChangeFieldTypes: setSelectedFieldTypes, + nameFilter, + onChangeNameFilter: setNameFilter, + screenReaderDescriptionId, + }, + onFilterField: + fieldSearchHighlight?.length || selectedFieldTypes.length > 0 + ? (field: T) => { + if ( + fieldSearchHighlight?.length && + !field.name?.toLowerCase().includes(fieldSearchHighlight) && + !field.displayName?.toLowerCase().includes(fieldSearchHighlight) + ) { + return false; + } + if (selectedFieldTypes.length > 0) { + return selectedFieldTypes.includes(getFieldIconType(field, getCustomFieldType)); + } + return true; + } + : undefined, + }; + }, [ + docLinks, + selectedFieldTypes, + allFields, + getCustomFieldType, + onSupportedFieldFilter, + setSelectedFieldTypes, + nameFilter, + setNameFilter, + screenReaderDescriptionId, + ]); +} diff --git a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx index df4b3f684647f..5fa7344955b52 100644 --- a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx +++ b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.test.tsx @@ -7,6 +7,7 @@ */ import { renderHook } from '@testing-library/react-hooks'; +import { act } from 'react-test-renderer'; import { stubDataViewWithoutTimeField, stubLogstashDataView as dataView, @@ -14,7 +15,10 @@ import { import { createStubDataView, stubFieldSpecMap } from '@kbn/data-plugin/public/stubs'; import { DataViewField } from '@kbn/data-views-plugin/common'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { coreMock } from '@kbn/core/public/mocks'; import { type GroupedFieldsParams, useGroupedFields } from './use_grouped_fields'; +import * as ExistenceApi from './use_existing_fields'; +import { type ExistingFieldsReader } from './use_existing_fields'; import { ExistenceFetchStatus, FieldListGroups, FieldsGroupNames } from '../types'; describe('UnifiedFieldList useGroupedFields()', () => { @@ -38,6 +42,7 @@ describe('UnifiedFieldList useGroupedFields()', () => { const dataViews = dataViewPluginMocks.createStartContract(); mockedServices = { dataViews, + core: coreMock.createStart(), }; dataViews.get.mockImplementation(async (id: string) => { @@ -57,10 +62,11 @@ describe('UnifiedFieldList useGroupedFields()', () => { await waitForNextUpdate(); - expect(result.current.fieldGroups).toMatchSnapshot(); - expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.unknown); - expect(result.current.fieldsExistInIndex).toBe(false); - expect(result.current.scrollToTopResetCounter).toBeTruthy(); + let fieldListGroupedProps = result.current.fieldListGroupedProps; + expect(fieldListGroupedProps.fieldGroups).toMatchSnapshot(); + expect(fieldListGroupedProps.fieldsExistenceStatus).toBe(ExistenceFetchStatus.unknown); + expect(fieldListGroupedProps.fieldsExistInIndex).toBe(false); + expect(fieldListGroupedProps.scrollToTopResetCounter).toBeTruthy(); rerender({ ...props, @@ -68,9 +74,10 @@ describe('UnifiedFieldList useGroupedFields()', () => { allFields: null, }); - expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.unknown); - expect(result.current.fieldsExistInIndex).toBe(true); - expect(result.current.scrollToTopResetCounter).toBeTruthy(); + fieldListGroupedProps = result.current.fieldListGroupedProps; + expect(fieldListGroupedProps.fieldsExistenceStatus).toBe(ExistenceFetchStatus.unknown); + expect(fieldListGroupedProps.fieldsExistInIndex).toBe(true); + expect(fieldListGroupedProps.scrollToTopResetCounter).toBeTruthy(); }); it('should work correctly for no data', async () => { @@ -79,13 +86,28 @@ describe('UnifiedFieldList useGroupedFields()', () => { allFields: [], services: mockedServices, }; + + jest.spyOn(ExistenceApi, 'useExistingFieldsReader').mockImplementation( + (): ExistingFieldsReader => ({ + hasFieldData: (dataViewId) => { + return dataViewId === props.dataViewId; + }, + getFieldsExistenceStatus: (dataViewId) => + dataViewId === props.dataViewId + ? ExistenceFetchStatus.succeeded + : ExistenceFetchStatus.unknown, + isFieldsExistenceInfoUnavailable: (dataViewId) => dataViewId !== props.dataViewId, + }) + ); + const { result, waitForNextUpdate, rerender } = renderHook(useGroupedFields, { initialProps: props, }); await waitForNextUpdate(); - const fieldGroups = result.current.fieldGroups; + let fieldListGroupedProps = result.current.fieldListGroupedProps; + const fieldGroups = fieldListGroupedProps.fieldGroups; expect( Object.keys(fieldGroups!).map( @@ -102,8 +124,8 @@ describe('UnifiedFieldList useGroupedFields()', () => { ]); expect(fieldGroups).toMatchSnapshot(); - expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); - expect(result.current.fieldsExistInIndex).toBe(false); + expect(fieldListGroupedProps.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); + expect(fieldListGroupedProps.fieldsExistInIndex).toBe(false); rerender({ ...props, @@ -111,8 +133,11 @@ describe('UnifiedFieldList useGroupedFields()', () => { allFields: [], }); - expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); - expect(result.current.fieldsExistInIndex).toBe(true); + fieldListGroupedProps = result.current.fieldListGroupedProps; + expect(fieldListGroupedProps.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); + expect(fieldListGroupedProps.fieldsExistInIndex).toBe(true); + + (ExistenceApi.useExistingFieldsReader as jest.Mock).mockRestore(); }); it('should work correctly with fields', async () => { @@ -121,13 +146,28 @@ describe('UnifiedFieldList useGroupedFields()', () => { allFields, services: mockedServices, }; + + jest.spyOn(ExistenceApi, 'useExistingFieldsReader').mockImplementation( + (): ExistingFieldsReader => ({ + hasFieldData: (dataViewId) => { + return dataViewId === props.dataViewId; + }, + getFieldsExistenceStatus: (dataViewId) => + dataViewId === props.dataViewId + ? ExistenceFetchStatus.succeeded + : ExistenceFetchStatus.unknown, + isFieldsExistenceInfoUnavailable: (dataViewId) => dataViewId !== props.dataViewId, + }) + ); + const { result, waitForNextUpdate, rerender } = renderHook(useGroupedFields, { initialProps: props, }); await waitForNextUpdate(); - const fieldGroups = result.current.fieldGroups; + let fieldListGroupedProps = result.current.fieldListGroupedProps; + const fieldGroups = fieldListGroupedProps.fieldGroups; expect( Object.keys(fieldGroups!).map( @@ -143,8 +183,8 @@ describe('UnifiedFieldList useGroupedFields()', () => { 'MetaFields-3', ]); - expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); - expect(result.current.fieldsExistInIndex).toBe(true); + expect(fieldListGroupedProps.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); + expect(fieldListGroupedProps.fieldsExistInIndex).toBe(true); rerender({ ...props, @@ -152,8 +192,11 @@ describe('UnifiedFieldList useGroupedFields()', () => { allFields, }); - expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); - expect(result.current.fieldsExistInIndex).toBe(true); + fieldListGroupedProps = result.current.fieldListGroupedProps; + expect(fieldListGroupedProps.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); + expect(fieldListGroupedProps.fieldsExistInIndex).toBe(true); + + (ExistenceApi.useExistingFieldsReader as jest.Mock).mockRestore(); }); it('should work correctly when filtered', async () => { @@ -168,8 +211,9 @@ describe('UnifiedFieldList useGroupedFields()', () => { await waitForNextUpdate(); - let fieldGroups = result.current.fieldGroups; - const scrollToTopResetCounter1 = result.current.scrollToTopResetCounter; + const fieldListGroupedProps = result.current.fieldListGroupedProps; + let fieldGroups = fieldListGroupedProps.fieldGroups; + const scrollToTopResetCounter1 = fieldListGroupedProps.scrollToTopResetCounter; expect( Object.keys(fieldGroups!).map( @@ -193,7 +237,7 @@ describe('UnifiedFieldList useGroupedFields()', () => { onFilterField: (field: DataViewField) => field.name.startsWith('@'), }); - fieldGroups = result.current.fieldGroups; + fieldGroups = result.current.fieldListGroupedProps.fieldGroups; expect( Object.keys(fieldGroups!).map( @@ -212,7 +256,9 @@ describe('UnifiedFieldList useGroupedFields()', () => { 'MetaFields-0-3', ]); - expect(result.current.scrollToTopResetCounter).not.toBe(scrollToTopResetCounter1); + expect(result.current.fieldListGroupedProps.scrollToTopResetCounter).not.toBe( + scrollToTopResetCounter1 + ); }); it('should not change the scroll position if fields list is extended', async () => { @@ -227,14 +273,16 @@ describe('UnifiedFieldList useGroupedFields()', () => { await waitForNextUpdate(); - const scrollToTopResetCounter1 = result.current.scrollToTopResetCounter; + const scrollToTopResetCounter1 = result.current.fieldListGroupedProps.scrollToTopResetCounter; rerender({ ...props, allFields: allFieldsIncludingUnmapped, }); - expect(result.current.scrollToTopResetCounter).toBe(scrollToTopResetCounter1); + expect(result.current.fieldListGroupedProps.scrollToTopResetCounter).toBe( + scrollToTopResetCounter1 + ); }); it('should work correctly when custom unsupported fields are skipped', async () => { @@ -249,7 +297,7 @@ describe('UnifiedFieldList useGroupedFields()', () => { await waitForNextUpdate(); - const fieldGroups = result.current.fieldGroups; + const fieldGroups = result.current.fieldListGroupedProps.fieldGroups; expect( Object.keys(fieldGroups!).map( @@ -279,7 +327,7 @@ describe('UnifiedFieldList useGroupedFields()', () => { await waitForNextUpdate(); - const fieldGroups = result.current.fieldGroups; + const fieldGroups = result.current.fieldListGroupedProps.fieldGroups; expect( Object.keys(fieldGroups!).map( @@ -305,7 +353,8 @@ describe('UnifiedFieldList useGroupedFields()', () => { }, }); - const fieldGroups = result.current.fieldGroups; + const fieldListGroupedProps = result.current.fieldListGroupedProps; + const fieldGroups = fieldListGroupedProps.fieldGroups; expect( Object.keys(fieldGroups!).map( @@ -320,8 +369,8 @@ describe('UnifiedFieldList useGroupedFields()', () => { 'MetaFields-0', ]); - expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); - expect(result.current.fieldsExistInIndex).toBe(true); + expect(fieldListGroupedProps.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); + expect(fieldListGroupedProps.fieldsExistInIndex).toBe(true); }); it('should work correctly when details are overwritten', async () => { @@ -344,7 +393,7 @@ describe('UnifiedFieldList useGroupedFields()', () => { await waitForNextUpdate(); - const fieldGroups = result.current.fieldGroups; + const fieldGroups = result.current.fieldListGroupedProps.fieldGroups; expect(fieldGroups[FieldsGroupNames.SelectedFields]?.helpText).toBe('test'); expect(fieldGroups[FieldsGroupNames.AvailableFields]?.helpText).not.toBe('test'); @@ -358,7 +407,10 @@ describe('UnifiedFieldList useGroupedFields()', () => { dataViewId: dataView.id!, allFields, services: mockedServices, - fieldsExistenceReader: { + }; + + jest.spyOn(ExistenceApi, 'useExistingFieldsReader').mockImplementation( + (): ExistingFieldsReader => ({ hasFieldData: (dataViewId, fieldName) => { return dataViewId === knownDataViewId && ['bytes', 'extension'].includes(fieldName); }, @@ -367,15 +419,16 @@ describe('UnifiedFieldList useGroupedFields()', () => { ? ExistenceFetchStatus.succeeded : ExistenceFetchStatus.unknown, isFieldsExistenceInfoUnavailable: (dataViewId) => dataViewId !== knownDataViewId, - }, - }; + }) + ); const { result, waitForNextUpdate, rerender } = renderHook(useGroupedFields, { initialProps: props, }); await waitForNextUpdate(); - fieldGroups = result.current.fieldGroups; + let fieldListGroupedProps = result.current.fieldListGroupedProps; + fieldGroups = fieldListGroupedProps.fieldGroups; expect( Object.keys(fieldGroups!).map( @@ -391,8 +444,8 @@ describe('UnifiedFieldList useGroupedFields()', () => { 'MetaFields-3', ]); - expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); - expect(result.current.fieldsExistInIndex).toBe(true); + expect(fieldListGroupedProps.fieldsExistenceStatus).toBe(ExistenceFetchStatus.succeeded); + expect(fieldListGroupedProps.fieldsExistInIndex).toBe(true); rerender({ ...props, @@ -402,7 +455,8 @@ describe('UnifiedFieldList useGroupedFields()', () => { await waitForNextUpdate(); - fieldGroups = result.current.fieldGroups; + fieldListGroupedProps = result.current.fieldListGroupedProps; + fieldGroups = fieldListGroupedProps.fieldGroups; expect( Object.keys(fieldGroups!).map( @@ -417,8 +471,10 @@ describe('UnifiedFieldList useGroupedFields()', () => { 'MetaFields-0', ]); - expect(result.current.fieldsExistenceStatus).toBe(ExistenceFetchStatus.unknown); - expect(result.current.fieldsExistInIndex).toBe(true); + expect(fieldListGroupedProps.fieldsExistenceStatus).toBe(ExistenceFetchStatus.unknown); + expect(fieldListGroupedProps.fieldsExistInIndex).toBe(true); + + (ExistenceApi.useExistingFieldsReader as jest.Mock).mockRestore(); }); it('should work correctly when popular fields limit is present', async () => { @@ -437,7 +493,7 @@ describe('UnifiedFieldList useGroupedFields()', () => { await waitForNextUpdate(); - const fieldGroups = result.current.fieldGroups; + const fieldGroups = result.current.fieldListGroupedProps.fieldGroups; expect( Object.keys(fieldGroups!).map( @@ -470,7 +526,7 @@ describe('UnifiedFieldList useGroupedFields()', () => { await waitForNextUpdate(); - const fieldGroups = result.current.fieldGroups; + const fieldGroups = result.current.fieldListGroupedProps.fieldGroups; expect(fieldGroups).toMatchSnapshot(); }); @@ -485,7 +541,7 @@ describe('UnifiedFieldList useGroupedFields()', () => { await waitForNextUpdate(); - const fieldGroups = result.current.fieldGroups; + const fieldGroups = result.current.fieldListGroupedProps.fieldGroups; expect( Object.keys(fieldGroups!).map( @@ -519,7 +575,7 @@ describe('UnifiedFieldList useGroupedFields()', () => { await waitForNextUpdate(); - const fieldGroups = result.current.fieldGroups; + const fieldGroups = result.current.fieldListGroupedProps.fieldGroups; expect( Object.keys(fieldGroups!).map( @@ -537,4 +593,48 @@ describe('UnifiedFieldList useGroupedFields()', () => { expect(fieldGroups.SelectedFields?.fields).toBe(customSortedFields); }); + + it('should include filters props', async () => { + const { result, waitForNextUpdate } = renderHook(useGroupedFields, { + initialProps: { + dataViewId: dataView.id!, + allFields, + services: mockedServices, + }, + }); + + await waitForNextUpdate(); + + const { fieldListFiltersProps, fieldListGroupedProps } = result.current; + const fieldGroups = fieldListGroupedProps.fieldGroups; + + expect(fieldGroups.AvailableFields?.fields?.length).toBe(25); + expect(fieldGroups.AvailableFields?.fieldSearchHighlight).toBeUndefined(); + expect(fieldListFiltersProps.screenReaderDescriptionId).toBe( + fieldListGroupedProps.screenReaderDescriptionId + ); + + act(() => { + fieldListFiltersProps.onChangeNameFilter('Me'); + }); + + const { + fieldListFiltersProps: newFieldListFiltersProps, + fieldListGroupedProps: newFieldListGroupedProps, + } = result.current; + const newFieldGroups = newFieldListGroupedProps.fieldGroups; + expect(newFieldGroups.AvailableFields?.fields?.length).toBe(4); + expect(newFieldGroups.AvailableFields?.fieldSearchHighlight).toBe('me'); + expect(newFieldListFiltersProps.screenReaderDescriptionId).toBe( + newFieldListGroupedProps.screenReaderDescriptionId + ); + + act(() => { + newFieldListFiltersProps.onChangeFieldTypes?.(['date']); + }); + + expect(result.current.fieldListGroupedProps.fieldGroups.AvailableFields?.fields?.length).toBe( + 3 + ); + }); }); diff --git a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts index 39d1258ee62d8..9ac24aaa86063 100644 --- a/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts +++ b/src/plugins/unified_field_list/public/hooks/use_grouped_fields.ts @@ -9,6 +9,7 @@ import { groupBy } from 'lodash'; import { useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; +import { type CoreStart } from '@kbn/core-lifecycle-browser'; import { type DataView, type DataViewField } from '@kbn/data-views-plugin/common'; import { type DataViewsContract } from '@kbn/data-views-plugin/public'; import { @@ -19,55 +20,72 @@ import { FieldsGroupNames, ExistenceFetchStatus, } from '../types'; -import { type ExistingFieldsReader } from './use_existing_fields'; +import { useExistingFieldsReader } from './use_existing_fields'; +import { + useFieldFilters, + type FieldFiltersResult, + type FieldFiltersParams, +} from './use_field_filters'; export interface GroupedFieldsParams { dataViewId: string | null; // `null` is for text-based queries allFields: T[] | null; // `null` is for loading indicator services: { dataViews: DataViewsContract; + core: Pick; }; - fieldsExistenceReader?: ExistingFieldsReader; // use `undefined` for text-based queries isAffectedByGlobalFilter?: boolean; popularFieldsLimit?: number; sortedSelectedFields?: T[]; + getCustomFieldType?: FieldFiltersParams['getCustomFieldType']; onOverrideFieldGroupDetails?: ( groupName: FieldsGroupNames ) => Partial | undefined | null; onSupportedFieldFilter?: (field: T) => boolean; onSelectedFieldFilter?: (field: T) => boolean; - onFilterField?: (field: T) => boolean; + onFilterField?: (field: T) => boolean; // TODO: deprecate after integrating the unified field search and field filters into Discover } export interface GroupedFieldsResult { - fieldGroups: FieldListGroups; - scrollToTopResetCounter: number; - fieldsExistenceStatus: ExistenceFetchStatus; - fieldsExistInIndex: boolean; + fieldListFiltersProps: FieldFiltersResult['fieldListFiltersProps']; + fieldListGroupedProps: { + fieldGroups: FieldListGroups; + scrollToTopResetCounter: number; + fieldsExistenceStatus: ExistenceFetchStatus; + fieldsExistInIndex: boolean; + screenReaderDescriptionId?: string; + }; } export function useGroupedFields({ dataViewId, allFields, services, - fieldsExistenceReader, isAffectedByGlobalFilter = false, popularFieldsLimit, sortedSelectedFields, + getCustomFieldType, onOverrideFieldGroupDetails, onSupportedFieldFilter, onSelectedFieldFilter, onFilterField, }: GroupedFieldsParams): GroupedFieldsResult { + const fieldsExistenceReader = useExistingFieldsReader(); + const fieldListFilters = useFieldFilters({ + allFields, + services, + getCustomFieldType, + onSupportedFieldFilter, + }); + const onFilterFieldList = onFilterField ?? fieldListFilters.onFilterField; const [dataView, setDataView] = useState(null); const isAffectedByTimeFilter = Boolean(dataView?.timeFieldName); const fieldsExistenceInfoUnavailable: boolean = dataViewId - ? fieldsExistenceReader?.isFieldsExistenceInfoUnavailable(dataViewId) ?? false + ? fieldsExistenceReader.isFieldsExistenceInfoUnavailable(dataViewId) : true; - const hasFieldDataHandler = - dataViewId && fieldsExistenceReader - ? fieldsExistenceReader.hasFieldData - : hasFieldDataByDefault; + const hasFieldDataHandler = dataViewId + ? fieldsExistenceReader.hasFieldData + : hasFieldDataByDefault; useEffect(() => { const getDataView = async () => { @@ -86,8 +104,11 @@ export function useGroupedFields({ } }, [dataView, setDataView, dataViewId]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const scrollToTopResetCounter: number = useMemo(() => Date.now(), [dataViewId, onFilterField]); + const scrollToTopResetCounter: number = useMemo( + () => Date.now(), + // eslint-disable-next-line react-hooks/exhaustive-deps + [dataViewId, onFilterFieldList] + ); const unfilteredFieldGroups: FieldListGroups = useMemo(() => { const containsData = (field: T) => { @@ -297,17 +318,21 @@ export function useGroupedFields({ ]); const fieldGroups: FieldListGroups = useMemo(() => { - if (!onFilterField) { + if (!onFilterFieldList) { return unfilteredFieldGroups; } return Object.fromEntries( Object.entries(unfilteredFieldGroups).map(([name, group]) => [ name, - { ...group, fields: group.fields.filter(onFilterField) }, + { + ...group, + fieldSearchHighlight: fieldListFilters.fieldSearchHighlight, + fields: group.fields.filter(onFilterFieldList), + }, ]) ) as FieldListGroups; - }, [unfilteredFieldGroups, onFilterField]); + }, [unfilteredFieldGroups, onFilterFieldList, fieldListFilters.fieldSearchHighlight]); const hasDataLoaded = Boolean(allFields); const allFieldsLength = allFields?.length; @@ -327,14 +352,28 @@ export function useGroupedFields({ return fieldsExistenceReader.getFieldsExistenceStatus(dataViewId); }, [dataViewId, hasDataLoaded, fieldsExistenceReader]); - return useMemo(() => { + const screenReaderDescriptionId = + fieldListFilters.fieldListFiltersProps.screenReaderDescriptionId; + const fieldListGroupedProps = useMemo(() => { return { fieldGroups, scrollToTopResetCounter, fieldsExistInIndex, fieldsExistenceStatus, + screenReaderDescriptionId, }; - }, [fieldGroups, scrollToTopResetCounter, fieldsExistInIndex, fieldsExistenceStatus]); + }, [ + fieldGroups, + scrollToTopResetCounter, + fieldsExistInIndex, + fieldsExistenceStatus, + screenReaderDescriptionId, + ]); + + return { + fieldListGroupedProps, + fieldListFiltersProps: fieldListFilters.fieldListFiltersProps, + }; } const collator = new Intl.Collator(undefined, { diff --git a/src/plugins/unified_field_list/public/index.ts b/src/plugins/unified_field_list/public/index.ts index 68fddef0ffc16..8c1873c8f49ef 100755 --- a/src/plugins/unified_field_list/public/index.ts +++ b/src/plugins/unified_field_list/public/index.ts @@ -13,7 +13,10 @@ export type { NumberStatsResult, TopValuesResult, } from '../common/types'; -export { FieldListGrouped, type FieldListGroupedProps } from './components/field_list'; +export { FieldList, type FieldListProps } from './components/field_list'; +export { FieldListGrouped, type FieldListGroupedProps } from './components/field_list_grouped'; +export { FieldListFilters, type FieldListFiltersProps } from './components/field_list_filters'; +export { FieldIcon, type FieldIconProps, getFieldIconProps } from './components/field_icon'; export type { FieldTopValuesBucketProps, FieldTopValuesBucketParams, @@ -55,6 +58,8 @@ export type { AddFieldFilterHandler, FieldListGroups, FieldsGroupDetails, + FieldTypeKnown, + GetCustomFieldType, } from './types'; export { ExistenceFetchStatus, FieldsGroupNames } from './types'; @@ -74,9 +79,24 @@ export { type GroupedFieldsResult, } from './hooks/use_grouped_fields'; +export { + useFieldFilters, + type FieldFiltersParams, + type FieldFiltersResult, +} from './hooks/use_field_filters'; + export { useQuerySubscriber, hasQuerySubscriberData, type QuerySubscriberResult, type QuerySubscriberParams, } from './hooks/use_query_subscriber'; + +export { wrapFieldNameOnDot } from './utils/wrap_field_name_on_dot'; +export { + getFieldTypeName, + getFieldTypeDescription, + KNOWN_FIELD_TYPES, + getFieldType, + getFieldIconType, +} from './utils/field_types'; diff --git a/src/plugins/unified_field_list/public/types.ts b/src/plugins/unified_field_list/public/types.ts index c28452ebc6f25..8e7ae0b9e8734 100755 --- a/src/plugins/unified_field_list/public/types.ts +++ b/src/plugins/unified_field_list/public/types.ts @@ -30,6 +30,9 @@ export interface FieldListItem { type?: DataViewField['type']; displayName?: DataViewField['displayName']; count?: DataViewField['count']; + timeSeriesMetric?: DataViewField['timeSeriesMetric']; + esTypes?: DataViewField['esTypes']; + scripted?: DataViewField['scripted']; } export enum FieldsGroupNames { @@ -57,8 +60,16 @@ export interface FieldsGroupDetails { export interface FieldsGroup extends FieldsGroupDetails { fields: T[]; fieldCount: number; + fieldSearchHighlight?: string; } export type FieldListGroups = { [key in FieldsGroupNames]?: FieldsGroup; }; + +export type FieldTypeKnown = Exclude< + DataViewField['timeSeriesMetric'] | DataViewField['type'], + undefined +>; + +export type GetCustomFieldType = (field: T) => FieldTypeKnown; diff --git a/src/plugins/unified_field_list/public/utils/field_types/field_types.ts b/src/plugins/unified_field_list/public/utils/field_types/field_types.ts new file mode 100644 index 0000000000000..612c3b43c4baa --- /dev/null +++ b/src/plugins/unified_field_list/public/utils/field_types/field_types.ts @@ -0,0 +1,41 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FieldTypeKnown } from '../../types'; + +/** + * Field types for which name and description are defined + * @public + */ +export enum KNOWN_FIELD_TYPES { + DOCUMENT = 'document', // "Records" on Lens page + BOOLEAN = 'boolean', + CONFLICT = 'conflict', + COUNTER = 'counter', + DATE = 'date', + DATE_RANGE = 'date_range', + GAUGE = 'gauge', + GEO_POINT = 'geo_point', + GEO_SHAPE = 'geo_shape', + HISTOGRAM = 'histogram', + IP = 'ip', + IP_RANGE = 'ip_range', + KEYWORD = 'keyword', + MURMUR3 = 'murmur3', + NUMBER = 'number', + NESTED = 'nested', + STRING = 'string', + TEXT = 'text', + VERSION = 'version', +} + +export const KNOWN_FIELD_TYPE_LIST: string[] = Object.values(KNOWN_FIELD_TYPES); + +export const isKnownFieldType = (type?: string): type is FieldTypeKnown => { + return !!type && KNOWN_FIELD_TYPE_LIST.includes(type); +}; diff --git a/src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.test.ts b/src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.test.ts new file mode 100644 index 0000000000000..82da142c03d3b --- /dev/null +++ b/src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.test.ts @@ -0,0 +1,48 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DataViewField } from '@kbn/data-views-plugin/common'; +import { getFieldIconType } from './get_field_icon_type'; + +describe('UnifiedFieldList getFieldIconType()', () => { + it('extracts type for non-string types', () => { + expect( + getFieldIconType({ + type: 'not-string', + esTypes: ['bar'], + } as DataViewField) + ).toBe('not-string'); + }); + + it('extracts type when type is string but esTypes is unavailable', () => { + expect( + getFieldIconType({ + type: 'string', + esTypes: undefined, + } as DataViewField) + ).toBe('string'); + }); + + it('extracts esType when type is string and esTypes is available', () => { + expect( + getFieldIconType({ + type: 'string', + esTypes: ['version'], + } as DataViewField) + ).toBe('version'); + }); + + it('extracts type for meta fields', () => { + expect( + getFieldIconType({ + type: 'string', + esTypes: ['_id'], + } as DataViewField) + ).toBe('string'); + }); +}); diff --git a/src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.ts b/src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.ts new file mode 100644 index 0000000000000..ef843012e0c48 --- /dev/null +++ b/src/plugins/unified_field_list/public/utils/field_types/get_field_icon_type.ts @@ -0,0 +1,29 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { type DataViewField } from '@kbn/data-views-plugin/common'; +import type { FieldListItem, GetCustomFieldType } from '../../types'; +import { getFieldType } from './get_field_type'; + +/** + * Returns an icon type for a field + * @param field + * @param getCustomFieldType + * @public + */ +export function getFieldIconType( + field: T, + getCustomFieldType?: GetCustomFieldType +): string { + const type = getCustomFieldType ? getCustomFieldType(field) : getFieldType(field); + const esType = field.esTypes?.[0] || null; + if (esType && ['_id', '_index'].includes(esType)) { + return type; + } + return type === 'string' && esType ? esType : type; +} diff --git a/src/plugins/unified_field_list/public/utils/field_types/get_field_type.test.ts b/src/plugins/unified_field_list/public/utils/field_types/get_field_type.test.ts new file mode 100644 index 0000000000000..99e109603132f --- /dev/null +++ b/src/plugins/unified_field_list/public/utils/field_types/get_field_type.test.ts @@ -0,0 +1,29 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DataViewField } from '@kbn/data-views-plugin/common'; +import { getFieldType } from './get_field_type'; + +describe('UnifiedFieldList getFieldType()', () => { + it('uses time series metric if set', () => { + expect( + getFieldType({ + type: 'string', + timeSeriesMetric: 'histogram', + } as DataViewField) + ).toBe('histogram'); + }); + + it('returns field type otherwise', () => { + expect( + getFieldType({ + type: 'number', + } as DataViewField) + ).toBe('number'); + }); +}); diff --git a/src/plugins/unified_field_list/public/utils/field_types/get_field_type.ts b/src/plugins/unified_field_list/public/utils/field_types/get_field_type.ts new file mode 100644 index 0000000000000..3ff787188dd06 --- /dev/null +++ b/src/plugins/unified_field_list/public/utils/field_types/get_field_type.ts @@ -0,0 +1,22 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { type DataViewField } from '@kbn/data-views-plugin/common'; +import type { FieldListItem, FieldTypeKnown } from '../../types'; + +/** + * Returns a field type. Time series metric type will override the original field type. + * @param field + */ +export function getFieldType(field: T): FieldTypeKnown { + const timeSeriesMetric = field.timeSeriesMetric; + if (timeSeriesMetric) { + return timeSeriesMetric; + } + return field.type ?? 'string'; +} diff --git a/src/plugins/unified_field_list/public/utils/field_types/get_field_type_description.test.ts b/src/plugins/unified_field_list/public/utils/field_types/get_field_type_description.test.ts new file mode 100644 index 0000000000000..26434008e34ea --- /dev/null +++ b/src/plugins/unified_field_list/public/utils/field_types/get_field_type_description.test.ts @@ -0,0 +1,35 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getFieldTypeDescription, UNKNOWN_FIELD_TYPE_DESC } from './get_field_type_description'; +import { KNOWN_FIELD_TYPES } from './field_types'; + +describe('UnifiedFieldList getFieldTypeDescription()', () => { + describe('known field types should be recognized', () => { + it.each(Object.values(KNOWN_FIELD_TYPES))( + `'%s' should return a string that does not match '${UNKNOWN_FIELD_TYPE_DESC}'`, + (field) => { + const fieldTypeName = getFieldTypeDescription(field); + expect(typeof fieldTypeName).toBe('string'); + expect(fieldTypeName).not.toBe(UNKNOWN_FIELD_TYPE_DESC); + } + ); + }); + + it(`should return '${UNKNOWN_FIELD_TYPE_DESC}' when passed undefined`, () => { + expect(getFieldTypeDescription(undefined)).toBe(UNKNOWN_FIELD_TYPE_DESC); + }); + + it(`should return '${UNKNOWN_FIELD_TYPE_DESC}' when passed 'unknown'`, () => { + expect(getFieldTypeDescription('unknown')).toBe(UNKNOWN_FIELD_TYPE_DESC); + }); + + it('should return the original type string back when passed an unknown field type', () => { + expect(getFieldTypeDescription('unknown_field_type')).toBe('unknown_field_type'); + }); +}); diff --git a/src/plugins/unified_field_list/public/utils/field_types/get_field_type_description.ts b/src/plugins/unified_field_list/public/utils/field_types/get_field_type_description.ts new file mode 100644 index 0000000000000..a7b5b6ec3f05e --- /dev/null +++ b/src/plugins/unified_field_list/public/utils/field_types/get_field_type_description.ts @@ -0,0 +1,117 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { KBN_FIELD_TYPES } from '@kbn/field-types'; +import { KNOWN_FIELD_TYPES } from './field_types'; + +/** + * A user-friendly description of an unknown field type + */ +export const UNKNOWN_FIELD_TYPE_DESC = i18n.translate( + 'unifiedFieldList.fieldNameDescription.unknownField', + { + defaultMessage: 'Unknown field', + } +); + +/** + * Returns a user-friendly description of a field type + * @param type + * @public + */ +export function getFieldTypeDescription(type?: string) { + if (!type || type === KBN_FIELD_TYPES.UNKNOWN) { + return UNKNOWN_FIELD_TYPE_DESC; + } + + const knownType: KNOWN_FIELD_TYPES = type as KNOWN_FIELD_TYPES; + switch (knownType) { + case KNOWN_FIELD_TYPES.DOCUMENT: + return i18n.translate('unifiedFieldList.fieldNameDescription.recordField', { + defaultMessage: 'Number of records.', // TODO: add a better description + }); + case KNOWN_FIELD_TYPES.BOOLEAN: + return i18n.translate('unifiedFieldList.fieldNameDescription.booleanField', { + defaultMessage: 'True and false values.', + }); + case KNOWN_FIELD_TYPES.CONFLICT: + return i18n.translate('unifiedFieldList.fieldNameDescription.conflictField', { + defaultMessage: 'Field has values of different types. Resolve in Management > Data Views.', + }); + case KNOWN_FIELD_TYPES.COUNTER: + return i18n.translate('unifiedFieldList.fieldNameDescription.counterField', { + defaultMessage: 'Counter metric.', // TODO: add a better description + }); + case KNOWN_FIELD_TYPES.DATE: + return i18n.translate('unifiedFieldList.fieldNameDescription.dateField', { + defaultMessage: 'A date string or the number of seconds or milliseconds since 1/1/1970.', + }); + case KNOWN_FIELD_TYPES.DATE_RANGE: + return i18n.translate('unifiedFieldList.fieldNameDescription.dateRangeField', { + defaultMessage: 'Range of date values.', + }); + case KNOWN_FIELD_TYPES.GAUGE: + return i18n.translate('unifiedFieldList.fieldNameDescription.gaugeField', { + defaultMessage: 'Gauge metric.', // TODO: add a better description + }); + case KNOWN_FIELD_TYPES.GEO_POINT: + return i18n.translate('unifiedFieldList.fieldNameDescription.geoPointField', { + defaultMessage: 'Latitude and longitude points.', + }); + case KNOWN_FIELD_TYPES.GEO_SHAPE: + return i18n.translate('unifiedFieldList.fieldNameDescription.geoShapeField', { + defaultMessage: 'Complex shapes, such as polygons.', + }); + case KNOWN_FIELD_TYPES.HISTOGRAM: + return i18n.translate('unifiedFieldList.fieldNameDescription.histogramField', { + defaultMessage: 'Pre-aggregated numerical values in the form of a histogram.', + }); + case KNOWN_FIELD_TYPES.IP: + return i18n.translate('unifiedFieldList.fieldNameDescription.ipAddressField', { + defaultMessage: 'IPv4 and IPv6 addresses.', + }); + case KNOWN_FIELD_TYPES.IP_RANGE: + return i18n.translate('unifiedFieldList.fieldNameDescription.ipAddressRangeField', { + defaultMessage: 'Range of ip values supporting either IPv4 or IPv6 (or mixed) addresses.', + }); + case KNOWN_FIELD_TYPES.MURMUR3: + return i18n.translate('unifiedFieldList.fieldNameDescription.murmur3Field', { + defaultMessage: 'Field that computes and stores hashes of values.', + }); + case KNOWN_FIELD_TYPES.NUMBER: + return i18n.translate('unifiedFieldList.fieldNameDescription.numberField', { + defaultMessage: 'Long, integer, short, byte, double, and float values.', + }); + case KNOWN_FIELD_TYPES.STRING: + return i18n.translate('unifiedFieldList.fieldNameDescription.stringField', { + defaultMessage: 'Full text such as the body of an email or a product description.', + }); + case KNOWN_FIELD_TYPES.TEXT: + return i18n.translate('unifiedFieldList.fieldNameDescription.textField', { + defaultMessage: 'Full text such as the body of an email or a product description.', + }); + case KNOWN_FIELD_TYPES.KEYWORD: + return i18n.translate('unifiedFieldList.fieldNameDescription.keywordField', { + defaultMessage: + 'Structured content such as an ID, email address, hostname, status code, or tag.', + }); + case KNOWN_FIELD_TYPES.NESTED: + return i18n.translate('unifiedFieldList.fieldNameDescription.nestedField', { + defaultMessage: 'JSON object that preserves the relationship between its subfields.', + }); + case KNOWN_FIELD_TYPES.VERSION: + return i18n.translate('unifiedFieldList.fieldNameDescription.versionField', { + defaultMessage: 'Software versions. Supports "Semantic Versioning" precedence rules.', + }); + default: + // If you see a typescript error here, that's a sign that there are missing switch cases ^^ + const _exhaustiveCheck: never = knownType; + return knownType || _exhaustiveCheck; + } +} diff --git a/src/plugins/unified_field_list/public/utils/field_types/get_field_type_name.test.ts b/src/plugins/unified_field_list/public/utils/field_types/get_field_type_name.test.ts new file mode 100644 index 0000000000000..feed44a08aed2 --- /dev/null +++ b/src/plugins/unified_field_list/public/utils/field_types/get_field_type_name.test.ts @@ -0,0 +1,35 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getFieldTypeName, UNKNOWN_FIELD_TYPE_MESSAGE } from './get_field_type_name'; +import { KNOWN_FIELD_TYPES } from './field_types'; + +describe('UnifiedFieldList getFieldTypeName()', () => { + describe('known field types should be recognized', () => { + it.each(Object.values(KNOWN_FIELD_TYPES))( + `'%s' should return a string that does not match '${UNKNOWN_FIELD_TYPE_MESSAGE}'`, + (field) => { + const fieldTypeName = getFieldTypeName(field); + expect(typeof fieldTypeName).toBe('string'); + expect(fieldTypeName).not.toBe(UNKNOWN_FIELD_TYPE_MESSAGE); + } + ); + }); + + it(`should return '${UNKNOWN_FIELD_TYPE_MESSAGE}' when passed undefined`, () => { + expect(getFieldTypeName(undefined)).toBe(UNKNOWN_FIELD_TYPE_MESSAGE); + }); + + it(`should return '${UNKNOWN_FIELD_TYPE_MESSAGE}' when passed 'unknown'`, () => { + expect(getFieldTypeName('unknown')).toBe(UNKNOWN_FIELD_TYPE_MESSAGE); + }); + + it('should return the original type string back when passed an unknown field type', () => { + expect(getFieldTypeName('unknown_field_type')).toBe('unknown_field_type'); + }); +}); diff --git a/src/plugins/unified_field_list/public/utils/field_types/get_field_type_name.ts b/src/plugins/unified_field_list/public/utils/field_types/get_field_type_name.ts new file mode 100644 index 0000000000000..3b93b9847dfdd --- /dev/null +++ b/src/plugins/unified_field_list/public/utils/field_types/get_field_type_name.ts @@ -0,0 +1,123 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { KNOWN_FIELD_TYPES } from './field_types'; + +/** + * A user-friendly name of an unknown field type + */ +export const UNKNOWN_FIELD_TYPE_MESSAGE = i18n.translate( + 'unifiedFieldList.fieldNameIcons.unknownFieldAriaLabel', + { + defaultMessage: 'Unknown field', + } +); + +/** + * Returns a user-friendly name of a field type + * @param type + * @public + */ +export function getFieldTypeName(type?: string) { + if (!type || type === KBN_FIELD_TYPES.UNKNOWN) { + return UNKNOWN_FIELD_TYPE_MESSAGE; + } + + if (type === 'source') { + // Note that this type is currently not provided, type for _source is undefined + return i18n.translate('unifiedFieldList.fieldNameIcons.sourceFieldAriaLabel', { + defaultMessage: 'Source field', + }); + } + + const knownType: KNOWN_FIELD_TYPES = type as KNOWN_FIELD_TYPES; + switch (knownType) { + case KNOWN_FIELD_TYPES.DOCUMENT: + return i18n.translate('unifiedFieldList.fieldNameIcons.recordAriaLabel', { + defaultMessage: 'Records', // TODO: check if we need to rename it + }); + case KNOWN_FIELD_TYPES.BOOLEAN: + return i18n.translate('unifiedFieldList.fieldNameIcons.booleanAriaLabel', { + defaultMessage: 'Boolean', + }); + case KNOWN_FIELD_TYPES.CONFLICT: + return i18n.translate('unifiedFieldList.fieldNameIcons.conflictFieldAriaLabel', { + defaultMessage: 'Conflict', + }); + case KNOWN_FIELD_TYPES.COUNTER: + return i18n.translate('unifiedFieldList.fieldNameIcons.counterFieldAriaLabel', { + defaultMessage: 'Counter metric', + }); + case KNOWN_FIELD_TYPES.DATE: + return i18n.translate('unifiedFieldList.fieldNameIcons.dateFieldAriaLabel', { + defaultMessage: 'Date', + }); + case KNOWN_FIELD_TYPES.DATE_RANGE: + return i18n.translate('unifiedFieldList.fieldNameIcons.dateRangeFieldAriaLabel', { + defaultMessage: 'Date range', + }); + case KNOWN_FIELD_TYPES.GAUGE: + return i18n.translate('unifiedFieldList.fieldNameIcons.gaugeFieldAriaLabel', { + defaultMessage: 'Gauge metric', + }); + case KNOWN_FIELD_TYPES.GEO_POINT: + return i18n.translate('unifiedFieldList.fieldNameIcons.geoPointFieldAriaLabel', { + defaultMessage: 'Geo point', + }); + case KNOWN_FIELD_TYPES.GEO_SHAPE: + return i18n.translate('unifiedFieldList.fieldNameIcons.geoShapeFieldAriaLabel', { + defaultMessage: 'Geo shape', + }); + case KNOWN_FIELD_TYPES.HISTOGRAM: + return i18n.translate('unifiedFieldList.fieldNameIcons.histogramFieldAriaLabel', { + defaultMessage: 'Histogram', + }); + case KNOWN_FIELD_TYPES.IP: + return i18n.translate('unifiedFieldList.fieldNameIcons.ipAddressFieldAriaLabel', { + defaultMessage: 'IP address', + }); + case KNOWN_FIELD_TYPES.IP_RANGE: + return i18n.translate('unifiedFieldList.fieldNameIcons.ipRangeFieldAriaLabel', { + defaultMessage: 'IP range', + }); + case KNOWN_FIELD_TYPES.MURMUR3: + return i18n.translate('unifiedFieldList.fieldNameIcons.murmur3FieldAriaLabel', { + defaultMessage: 'Murmur3', + }); + case KNOWN_FIELD_TYPES.NUMBER: + return i18n.translate('unifiedFieldList.fieldNameIcons.numberFieldAriaLabel', { + defaultMessage: 'Number', + }); + case KNOWN_FIELD_TYPES.STRING: + return i18n.translate('unifiedFieldList.fieldNameIcons.stringFieldAriaLabel', { + defaultMessage: 'String', + }); + case KNOWN_FIELD_TYPES.TEXT: + return i18n.translate('unifiedFieldList.fieldNameIcons.textFieldAriaLabel', { + defaultMessage: 'Text', + }); + case KNOWN_FIELD_TYPES.KEYWORD: + return i18n.translate('unifiedFieldList.fieldNameIcons.keywordFieldAriaLabel', { + defaultMessage: 'Keyword', + }); + case KNOWN_FIELD_TYPES.NESTED: + return i18n.translate('unifiedFieldList.fieldNameIcons.nestedFieldAriaLabel', { + defaultMessage: 'Nested', + }); + case KNOWN_FIELD_TYPES.VERSION: + return i18n.translate('unifiedFieldList.fieldNameIcons.versionFieldAriaLabel', { + defaultMessage: 'Version', + }); + default: + // If you see a typescript error here, that's a sign that there are missing switch cases ^^ + const _exhaustiveCheck: never = knownType; + return knownType || _exhaustiveCheck; + } +} diff --git a/src/plugins/unified_field_list/public/utils/field_types/index.ts b/src/plugins/unified_field_list/public/utils/field_types/index.ts new file mode 100644 index 0000000000000..732d98e63f8f9 --- /dev/null +++ b/src/plugins/unified_field_list/public/utils/field_types/index.ts @@ -0,0 +1,13 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { KNOWN_FIELD_TYPES, KNOWN_FIELD_TYPE_LIST, isKnownFieldType } from './field_types'; +export { getFieldTypeName } from './get_field_type_name'; +export { getFieldTypeDescription } from './get_field_type_description'; +export { getFieldType } from './get_field_type'; +export { getFieldIconType } from './get_field_icon_type'; diff --git a/src/plugins/unified_field_list/public/utils/wrap_field_name_on_dot.test.ts b/src/plugins/unified_field_list/public/utils/wrap_field_name_on_dot.test.ts new file mode 100644 index 0000000000000..e8f675814b20f --- /dev/null +++ b/src/plugins/unified_field_list/public/utils/wrap_field_name_on_dot.test.ts @@ -0,0 +1,22 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { wrapFieldNameOnDot } from './wrap_field_name_on_dot'; + +describe('UnifiedFieldList wrapFieldNameOnDot()', () => { + it(`should work correctly for simple names`, () => { + expect(wrapFieldNameOnDot(dataView.getFieldByName('extension')?.name)).toBe('extension'); + }); + + it(`should work correctly for longer names`, () => { + expect(wrapFieldNameOnDot(dataView.getFieldByName('extension.keyword')?.name)).toBe( + 'extension.​keyword' + ); + }); +}); diff --git a/src/plugins/unified_field_list/public/utils/wrap_field_name_on_dot.ts b/src/plugins/unified_field_list/public/utils/wrap_field_name_on_dot.ts new file mode 100644 index 0000000000000..f67cfddb1a04c --- /dev/null +++ b/src/plugins/unified_field_list/public/utils/wrap_field_name_on_dot.ts @@ -0,0 +1,19 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Wraps field name on dot + * @param str + * @public + */ +export function wrapFieldNameOnDot(str?: string): string { + // u200B is a non-width white-space character, which allows + // the browser to efficiently word-wrap right after the dot + // without us having to draw a lot of extra DOM elements, etc + return str ? str.replace(/\./g, '.\u200B') : ''; +} diff --git a/x-pack/plugins/lens/public/data_views_service/loader.ts b/x-pack/plugins/lens/public/data_views_service/loader.ts index b7af8b5258a70..abd8a48815122 100644 --- a/x-pack/plugins/lens/public/data_views_service/loader.ts +++ b/x-pack/plugins/lens/public/data_views_service/loader.ts @@ -47,7 +47,7 @@ export function convertDataViewIntoLensIndexPattern( customLabel: field.customLabel, runtimeField: field.runtimeField, runtime: Boolean(field.runtimeField), - timeSeriesMetricType: field.timeSeriesMetric, + timeSeriesMetric: field.timeSeriesMetric, timeSeriesRollup: field.isRolledUpField, partiallyApplicableFunctions: field.isRolledUpField ? { diff --git a/x-pack/plugins/lens/public/datasources/form_based/datapanel.scss b/x-pack/plugins/lens/public/datasources/form_based/datapanel.scss index 32887d3f9350d..a408a462b9de3 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/datapanel.scss +++ b/x-pack/plugins/lens/public/datasources/form_based/datapanel.scss @@ -14,27 +14,6 @@ margin-bottom: $euiSizeS; } -.lnsInnerIndexPatternDataPanel__textField { - @include euiFormControlLayoutPadding(1, 'right'); - @include euiFormControlLayoutPadding(1, 'left'); - box-shadow: none; -} - -.lnsInnerIndexPatternDataPanel__filterType { - font-size: $euiFontSizeS; - padding: $euiSizeS; - border-bottom: 1px solid $euiColorLightestShade; -} - -.lnsInnerIndexPatternDataPanel__filterTypeInner { - display: flex; - align-items: center; - - .lnsFieldListPanel__fieldIcon { - margin-right: $euiSizeS; - } -} - .lnsChangeIndexPatternPopover { width: 320px; } @@ -48,7 +27,3 @@ display: inline; padding-left: 8px; } - -.lnsFilterButton .euiFilterButton__textShift { - min-width: 0; -} diff --git a/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx index 6639484ca6be4..411b8cd094ab2 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/datapanel.test.tsx @@ -926,7 +926,7 @@ describe('FormBased Data Panel', () => { const wrapper = await mountAndWaitForLazyModules(); act(() => { - wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"]').simulate('change', { + wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"] input').simulate('change', { target: { value: 'me' }, }); }); @@ -946,7 +946,7 @@ describe('FormBased Data Panel', () => { const wrapper = await mountAndWaitForLazyModules(); act(() => { - wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"]').simulate('change', { + wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"] input').simulate('change', { target: { value: 'me' }, }); }); @@ -965,7 +965,10 @@ describe('FormBased Data Panel', () => { it('should filter down by type', async () => { const wrapper = await mountAndWaitForLazyModules(); - wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click'); + wrapper + .find('[data-test-subj="lnsIndexPatternFieldTypeFilterToggle"]') + .last() + .simulate('click'); wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click'); @@ -977,7 +980,10 @@ describe('FormBased Data Panel', () => { it('should display no fields in groups when filtered by type Record', async () => { const wrapper = await mountAndWaitForLazyModules(); - wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click'); + wrapper + .find('[data-test-subj="lnsIndexPatternFieldTypeFilterToggle"]') + .last() + .simulate('click'); wrapper.find('[data-test-subj="typeFilter-document"]').first().simulate('click'); @@ -990,7 +996,10 @@ describe('FormBased Data Panel', () => { it('should toggle type if clicked again', async () => { const wrapper = await mountAndWaitForLazyModules(); - wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click'); + wrapper + .find('[data-test-subj="lnsIndexPatternFieldTypeFilterToggle"]') + .last() + .simulate('click'); wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click'); wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click'); @@ -1008,12 +1017,15 @@ describe('FormBased Data Panel', () => { const wrapper = await mountAndWaitForLazyModules(); act(() => { - wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"]').simulate('change', { + wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"] input').simulate('change', { target: { value: 'me' }, }); }); - wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click'); + wrapper + .find('[data-test-subj="lnsIndexPatternFieldTypeFilterToggle"]') + .last() + .simulate('click'); wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click'); diff --git a/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx b/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx index 26c9da50aa8be..01feaa4187627 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/datapanel.tsx @@ -7,20 +7,8 @@ import './datapanel.scss'; import { uniq } from 'lodash'; -import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { - EuiCallOut, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiFilterButton, - EuiFlexGroup, - EuiFlexItem, - EuiFormControlLayout, - EuiIcon, - EuiPopover, - EuiProgress, - htmlIdGenerator, -} from '@elastic/eui'; +import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { CoreStart } from '@kbn/core/public'; @@ -31,26 +19,23 @@ import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin import { VISUALIZE_GEO_FIELD_TRIGGER } from '@kbn/ui-actions-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { - FieldsGroupNames, + FieldList, + FieldListFilters, FieldListGrouped, type FieldListGroupedProps, + FieldsGroupNames, useExistingFieldsFetcher, useGroupedFields, - useExistingFieldsReader, } from '@kbn/unified-field-list-plugin/public'; import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import type { DatasourceDataPanelProps, - DataType, FramePublicAPI, IndexPattern, IndexPatternField, } from '../../types'; import { ChildDragDropProvider, DragContextState } from '../../drag_drop'; import type { FormBasedPrivateState } from './types'; -import { LensFieldIcon } from '../../shared_components/field_picker/lens_field_icon'; -import { getFieldType } from './pure_utils'; -import { fieldContainsData } from '../../shared_components'; import { IndexPatternServiceAPI } from '../../data_views_service/service'; import { FieldItem } from './field_item'; @@ -86,25 +71,6 @@ const supportedFieldTypes = new Set([ 'murmur3', ]); -const fieldTypeNames: Record = { - document: i18n.translate('xpack.lens.datatypes.record', { defaultMessage: 'Record' }), - string: i18n.translate('xpack.lens.datatypes.string', { defaultMessage: 'Text string' }), - number: i18n.translate('xpack.lens.datatypes.number', { defaultMessage: 'Number' }), - gauge: i18n.translate('xpack.lens.datatypes.gauge', { defaultMessage: 'Gauge metric' }), - counter: i18n.translate('xpack.lens.datatypes.counter', { defaultMessage: 'Counter metric' }), - boolean: i18n.translate('xpack.lens.datatypes.boolean', { defaultMessage: 'Boolean' }), - date: i18n.translate('xpack.lens.datatypes.date', { defaultMessage: 'Date' }), - ip: i18n.translate('xpack.lens.datatypes.ipAddress', { defaultMessage: 'IP address' }), - histogram: i18n.translate('xpack.lens.datatypes.histogram', { defaultMessage: 'Histogram' }), - geo_point: i18n.translate('xpack.lens.datatypes.geoPoint', { - defaultMessage: 'Geographic point', - }), - geo_shape: i18n.translate('xpack.lens.datatypes.geoShape', { - defaultMessage: 'Geographic shape', - }), - murmur3: i18n.translate('xpack.lens.datatypes.murmur3', { defaultMessage: 'murmur3' }), -}; - function onSupportedFieldFilter(field: IndexPatternField): boolean { return supportedFieldTypes.has(field.type); } @@ -200,18 +166,6 @@ export function FormBasedDataPanel({ ); } -interface DataPanelState { - nameFilter: string; - typeFilter: DataType[]; - isTypeFilterOpen: boolean; - isAvailableAccordionOpen: boolean; - isEmptyAccordionOpen: boolean; - isMetaAccordionOpen: boolean; -} - -const htmlId = htmlIdGenerator('datapanel'); -const fieldSearchDescriptionId = htmlId(); - export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ currentIndexPatternId, query, @@ -250,14 +204,6 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ layerFields?: string[]; activeIndexPatterns: IndexPattern[]; }) { - const [localState, setLocalState] = useState({ - nameFilter: '', - typeFilter: [], - isTypeFilterOpen: false, - isAvailableAccordionOpen: true, - isEmptyAccordionOpen: false, - isMetaAccordionOpen: false, - }); const { indexPatterns } = frame.dataViews; const currentIndexPattern = indexPatterns[currentIndexPatternId]; @@ -278,7 +224,6 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ } }, }); - const fieldsExistenceReader = useExistingFieldsReader(); const visualizeGeoFieldTrigger = uiActions.getTrigger(VISUALIZE_GEO_FIELD_TRIGGER); const allFields = useMemo(() => { @@ -290,13 +235,6 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ ); }, [currentIndexPattern, visualizeGeoFieldTrigger]); - const clearLocalState = () => setLocalState((s) => ({ ...s, nameFilter: '', typeFilter: [] })); - const availableFieldTypes = uniq([ - ...uniq(allFields.map(getFieldType)).filter((type) => type in fieldTypeNames), - // always include current field type filters - there may not be any fields of the type of an existing type filter on data view switch, but we still need to include the existing filter in the list so that the user can remove it - ...localState.typeFilter, - ]); - const editPermission = indexPatternFieldEditor.userPermissions.editIndexPattern() || !currentIndexPattern.isPersisted; @@ -307,23 +245,6 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ [layerFields] ); - const onFilterField = useCallback( - (field: IndexPatternField) => { - if ( - localState.nameFilter.length && - !field.name.toLowerCase().includes(localState.nameFilter.toLowerCase()) && - !field.displayName.toLowerCase().includes(localState.nameFilter.toLowerCase()) - ) { - return false; - } - if (localState.typeFilter.length > 0) { - return localState.typeFilter.includes(getFieldType(field) as DataType); - } - return true; - }, - [localState] - ); - const onOverrideFieldGroupDetails = useCallback( (groupName) => { if (groupName === FieldsGroupNames.AvailableFields) { @@ -345,15 +266,14 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ [core.uiSettings] ); - const fieldListGroupedProps = useGroupedFields({ + const { fieldListFiltersProps, fieldListGroupedProps } = useGroupedFields({ dataViewId: currentIndexPatternId, allFields, services: { dataViews, + core, }, - fieldsExistenceReader, isAffectedByGlobalFilter: Boolean(filters.length), - onFilterField, onSupportedFieldFilter, onSelectedFieldFilter, onOverrideFieldGroupDetails, @@ -459,14 +379,10 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({ ); const renderFieldItem: FieldListGroupedProps['renderFieldItem'] = useCallback( - ({ field, itemIndex, groupIndex, hideDetails }) => ( + ({ field, itemIndex, groupIndex, groupName, hideDetails, fieldSearchHighlight }) => ( - } > - {isProcessing && } - - { - clearLocalState(); - }, - }} - append={ - - setLocalState(() => ({ ...localState, isTypeFilterOpen: false })) - } - button={ - { - setLocalState((s) => ({ - ...s, - isTypeFilterOpen: !localState.isTypeFilterOpen, - })); - }} - > - - - } - > - ( - { - setLocalState((s) => ({ - ...s, - typeFilter: localState.typeFilter.includes(type) - ? localState.typeFilter.filter((t) => t !== type) - : [...localState.typeFilter, type], - })); - }} - > - - {fieldTypeNames[type]} - - - ))} - /> - - } - > - { - setLocalState({ ...localState, nameFilter: e.target.value }); - }} - aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameLabel', { - defaultMessage: 'Search field names', - description: 'Search the list of fields in the data view for the provided text', - })} - aria-describedby={fieldSearchDescriptionId} - /> - - - - - {...fieldListGroupedProps} - renderFieldItem={renderFieldItem} - screenReaderDescriptionForSearchInputId={fieldSearchDescriptionId} - data-test-subj="lnsIndexPattern" - /> - - + + {...fieldListGroupedProps} + renderFieldItem={renderFieldItem} + data-test-subj="lnsIndexPattern" + /> + ); }; diff --git a/x-pack/plugins/lens/public/datasources/form_based/field_item.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/field_item.test.tsx index 4d7064647eca3..1ce54610aeab8 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/field_item.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/field_item.test.tsx @@ -24,8 +24,8 @@ import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { loadFieldStats } from '@kbn/unified-field-list-plugin/public/services/field_stats'; +import { FieldIcon } from '@kbn/unified-field-list-plugin/public'; import { DOCUMENT_FIELD_NAME } from '../../../common'; -import { LensFieldIcon } from '../../shared_components'; import { FieldStats, FieldVisualizeButton } from '@kbn/unified-field-list-plugin/public'; jest.mock('@kbn/unified-field-list-plugin/public/services/field_stats', () => ({ @@ -204,12 +204,12 @@ describe('IndexPattern Field Item', () => { it('should show gauge icon for gauge fields', async () => { const wrapper = await getComponent({ ...defaultProps, - field: { ...defaultProps.field, timeSeriesMetricType: 'gauge' }, + field: { ...defaultProps.field, timeSeriesMetric: 'gauge' }, }); // Using .toContain over .toEqual because this element includes text from // which can't be seen, but shows in the text content - expect(wrapper.find(LensFieldIcon).first().prop('type')).toEqual('gauge'); + expect(wrapper.find(FieldIcon).first().prop('type')).toEqual('gauge'); }); it('should render edit field button if callback is set', async () => { diff --git a/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx b/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx index ff118afc84e32..f1d6d9e409902 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx @@ -24,17 +24,17 @@ import { FieldPopover, FieldPopoverHeader, FieldPopoverVisualize, + FieldIcon, + getFieldIconProps, + wrapFieldNameOnDot, } from '@kbn/unified-field-list-plugin/public'; import { generateFilters, getEsQueryConfig } from '@kbn/data-plugin/public'; import { DragDrop } from '../../drag_drop'; -import { DatasourceDataPanelProps, DataType } from '../../types'; +import { DatasourceDataPanelProps } from '../../types'; import { DOCUMENT_FIELD_NAME } from '../../../common'; import type { IndexPattern, IndexPatternField } from '../../types'; -import { LensFieldIcon } from '../../shared_components/field_picker/lens_field_icon'; import type { LensAppServices } from '../../app_plugin/types'; -import { debouncedComponent } from '../../debounced_component'; import { APP_ID } from '../../../common/constants'; -import { getFieldType } from './pure_utils'; import { combineQueryAndFilters } from '../../app_plugin/show_underlying_data'; export interface FieldItemProps { @@ -58,13 +58,6 @@ export interface FieldItemProps { uiActions: UiActionsStart; } -function wrapOnDot(str?: string) { - // u200B is a non-width white-space character, which allows - // the browser to efficiently word-wrap right after the dot - // without us having to draw a lot of extra DOM elements, etc - return str ? str.replace(/\./g, '.\u200B') : ''; -} - export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { const { field, @@ -157,7 +150,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { const order = useMemo(() => [0, groupIndex, itemIndex], [groupIndex, itemIndex]); - const lensFieldIcon = ; + const lensFieldIcon = ; const lensInfoIcon = ( - {wrapOnDot(field.displayName)} + + {wrapFieldNameOnDot(field.displayName)} } fieldInfoIcon={lensInfoIcon} @@ -275,7 +268,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { ); }; -export const FieldItem = debouncedComponent(InnerFieldItem); +export const FieldItem = React.memo(InnerFieldItem); function FieldItemPopoverContents( props: FieldItemProps & { diff --git a/x-pack/plugins/lens/public/datasources/form_based/pure_utils.ts b/x-pack/plugins/lens/public/datasources/form_based/pure_utils.ts index 5d8d823c30ab4..b13fd50a45e73 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/pure_utils.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/pure_utils.ts @@ -27,8 +27,8 @@ export function hasField(column: BaseIndexPatternColumn): column is FieldBasedIn } export function getFieldType(field: IndexPatternField) { - if (field.timeSeriesMetricType) { - return field.timeSeriesMetricType; + if (field.timeSeriesMetric) { + return field.timeSeriesMetric; } return field.type; } diff --git a/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx b/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx index cac4fc380cc99..9e9b76ceaf0ff 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/datapanel.test.tsx @@ -18,7 +18,7 @@ import { Start as DataViewPublicStart, } from '@kbn/data-views-plugin/public/mocks'; import type { DatatableColumn } from '@kbn/expressions-plugin/public'; -import { FieldButton } from '@kbn/react-field'; +import { EuiHighlight } from '@elastic/eui'; import { type TextBasedDataPanelProps, TextBasedDataPanel } from './datapanel'; @@ -208,7 +208,9 @@ describe('TextBased Query Languages Data Panel', () => { it('should render a search box', async () => { const wrapper = await mountAndWaitForLazyModules(); - expect(wrapper.find('[data-test-subj="lnsTextBasedLanguagesFieldSearch"]').length).toEqual(1); + expect( + wrapper.find('[data-test-subj="lnsTextBasedLanguagesFieldSearch"] input').length + ).toEqual(1); }); it('should list all supported fields in the pattern', async () => { @@ -217,8 +219,8 @@ describe('TextBased Query Languages Data Panel', () => { expect( wrapper .find('[data-test-subj="lnsTextBasedLanguagesAvailableFields"]') - .find(FieldButton) - .map((fieldItem) => fieldItem.prop('fieldName')) + .find(EuiHighlight) + .map((item) => item.prop('children')) ).toEqual(['bytes', 'memory', 'timestamp']); expect(wrapper.find('[data-test-subj="lnsTextBasedLanguagesEmptyFields"]').exists()).toBe( @@ -250,20 +252,26 @@ describe('TextBased Query Languages Data Panel', () => { it('should list all supported fields in the pattern that match the search input', async () => { const wrapper = await mountAndWaitForLazyModules(); - const searchBox = wrapper.find('[data-test-subj="lnsTextBasedLanguagesFieldSearch"]'); + expect( + wrapper + .find('[data-test-subj="lnsTextBasedLanguagesAvailableFields"]') + .find(EuiHighlight) + .map((item) => item.prop('children')) + ).toEqual(['bytes', 'memory', 'timestamp']); act(() => { - searchBox.prop('onChange')!({ + wrapper.find('[data-test-subj="lnsTextBasedLanguagesFieldSearch"] input').simulate('change', { target: { value: 'mem' }, - } as React.ChangeEvent); + }); }); await wrapper.update(); + expect( wrapper .find('[data-test-subj="lnsTextBasedLanguagesAvailableFields"]') - .find(FieldButton) - .map((fieldItem) => fieldItem.prop('fieldName')) + .find(EuiHighlight) + .map((item) => item.prop('children')) ).toEqual(['memory']); }); }); diff --git a/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx b/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx index ee480310e9ca3..ce4bbf80d7408 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/datapanel.tsx @@ -6,7 +6,7 @@ */ import React, { useCallback, useEffect, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFormControlLayout, htmlIdGenerator } from '@elastic/eui'; +import { EuiHighlight } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import usePrevious from 'react-use/lib/usePrevious'; import { isEqual } from 'lodash'; @@ -17,6 +17,11 @@ import { isOfAggregateQueryType } from '@kbn/es-query'; import { DatatableColumn, ExpressionsStart } from '@kbn/expressions-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { + FieldList, + FieldListFilters, + FieldIcon, + GetCustomFieldType, + wrapFieldNameOnDot, FieldListGrouped, FieldListGroupedProps, FieldsGroupNames, @@ -27,8 +32,8 @@ import type { DatasourceDataPanelProps } from '../../types'; import type { TextBasedPrivateState } from './types'; import { getStateFromAggregateQuery } from './utils'; import { ChildDragDropProvider, DragDrop } from '../../drag_drop'; -import { DataType } from '../../types'; -import { LensFieldIcon } from '../../shared_components'; + +const getCustomFieldType: GetCustomFieldType = (field) => field?.meta.type; export type TextBasedDataPanelProps = DatasourceDataPanelProps & { data: DataPublicPluginStart; @@ -36,8 +41,6 @@ export type TextBasedDataPanelProps = DatasourceDataPanelProps setLocalState((s) => ({ ...s, nameFilter: '' })); useEffect(() => { async function fetchData() { if (query && isOfAggregateQueryType(query) && !isEqual(query, prevQuery)) { @@ -83,19 +84,6 @@ export function TextBasedDataPanel({ [layerFields] ); - const onFilterField = useCallback( - (field: DatatableColumn) => { - if ( - localState.nameFilter && - !field.name.toLowerCase().includes(localState.nameFilter.toLowerCase()) - ) { - return false; - } - return true; - }, - [localState] - ); - const onOverrideFieldGroupDetails = useCallback((groupName) => { if (groupName === FieldsGroupNames.AvailableFields) { return { @@ -107,36 +95,44 @@ export function TextBasedDataPanel({ } }, []); - const fieldListGroupedProps = useGroupedFields({ + const { fieldListFiltersProps, fieldListGroupedProps } = useGroupedFields({ dataViewId: null, allFields: dataHasLoaded ? fieldList : null, services: { dataViews, + core, }, - onFilterField, + getCustomFieldType, onSelectedFieldFilter, onOverrideFieldGroupDetails, }); const renderFieldItem: FieldListGroupedProps['renderFieldItem'] = useCallback( - ({ field, itemIndex, groupIndex, hideDetails }) => { + ({ field, itemIndex, fieldSearchHighlight }) => { + if (!field) { + return <>; + } return ( {}} - fieldIcon={} - fieldName={field?.name} + fieldIcon={} + fieldName={ + + {wrapFieldNameOnDot(field.name)} + + } /> ); @@ -151,56 +147,19 @@ export function TextBasedDataPanel({ }} > - + } > - - { - clearLocalState(); - }, - }} - > - { - setLocalState({ ...localState, nameFilter: e.target.value }); - }} - aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameLabel', { - defaultMessage: 'Search field names', - description: 'Search the list of fields in the data view for the provided text', - })} - aria-describedby={fieldSearchDescriptionId} - /> - - - - - {...fieldListGroupedProps} - renderFieldItem={renderFieldItem} - screenReaderDescriptionForSearchInputId={fieldSearchDescriptionId} - data-test-subj="lnsTextBasedLanguages" - /> - - + + {...fieldListGroupedProps} + renderFieldItem={renderFieldItem} + data-test-subj="lnsTextBasedLanguages" + /> + ); diff --git a/x-pack/plugins/lens/public/shared_components/field_picker/__snapshots__/lens_field_icon.test.tsx.snap b/x-pack/plugins/lens/public/shared_components/field_picker/__snapshots__/lens_field_icon.test.tsx.snap deleted file mode 100644 index 8bbe49b2e0d7f..0000000000000 --- a/x-pack/plugins/lens/public/shared_components/field_picker/__snapshots__/lens_field_icon.test.tsx.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LensFieldIcon accepts FieldIcon props 1`] = ` - -`; - -exports[`LensFieldIcon renders properly 1`] = ` - -`; diff --git a/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx b/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx index 0d73bc3c132de..7be1f1bd1a7bb 100644 --- a/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx +++ b/x-pack/plugins/lens/public/shared_components/field_picker/field_picker.tsx @@ -10,9 +10,9 @@ import React, { useRef } from 'react'; import { i18n } from '@kbn/i18n'; import useEffectOnce from 'react-use/lib/useEffectOnce'; import { EuiComboBox, EuiComboBoxProps, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FieldIcon } from '@kbn/unified-field-list-plugin/public'; import classNames from 'classnames'; import { DataType } from '../../types'; -import { LensFieldIcon } from './lens_field_icon'; import { TruncatedLabel } from './truncated_label'; import type { FieldOptionValue, FieldOption } from './types'; @@ -118,7 +118,7 @@ export function FieldPicker({ return ( - diff --git a/x-pack/plugins/lens/public/shared_components/field_picker/index.ts b/x-pack/plugins/lens/public/shared_components/field_picker/index.ts index d9e18182dd0d8..f434f885f44d4 100644 --- a/x-pack/plugins/lens/public/shared_components/field_picker/index.ts +++ b/x-pack/plugins/lens/public/shared_components/field_picker/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export { LensFieldIcon } from './lens_field_icon'; export { FieldPicker } from './field_picker'; export { TruncatedLabel } from './truncated_label'; export type { FieldOptionValue, FieldOption } from './types'; diff --git a/x-pack/plugins/lens/public/shared_components/field_picker/lens_field_icon.test.tsx b/x-pack/plugins/lens/public/shared_components/field_picker/lens_field_icon.test.tsx deleted file mode 100644 index 120c24533665e..0000000000000 --- a/x-pack/plugins/lens/public/shared_components/field_picker/lens_field_icon.test.tsx +++ /dev/null @@ -1,20 +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 from 'react'; -import { shallow } from 'enzyme'; -import { LensFieldIcon } from './lens_field_icon'; - -test('LensFieldIcon renders properly', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); -}); - -test('LensFieldIcon accepts FieldIcon props', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); -}); diff --git a/x-pack/plugins/lens/public/shared_components/field_picker/lens_field_icon.tsx b/x-pack/plugins/lens/public/shared_components/field_picker/lens_field_icon.tsx deleted file mode 100644 index e3be012647e8c..0000000000000 --- a/x-pack/plugins/lens/public/shared_components/field_picker/lens_field_icon.tsx +++ /dev/null @@ -1,21 +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 from 'react'; -import { FieldIcon, FieldIconProps } from '@kbn/react-field'; -import { DataType } from '../../types'; -import { normalizeOperationDataType } from '../../datasources/form_based/pure_utils'; - -export function LensFieldIcon({ type, ...rest }: FieldIconProps & { type: DataType }) { - return ( - - ); -} diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index e57a18b3ee2ee..9138c61368795 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -9,7 +9,7 @@ export type { ToolbarPopoverProps } from './toolbar_popover'; export { ToolbarPopover } from './toolbar_popover'; export { LegendSettingsPopover } from './legend_settings_popover'; export { PalettePicker } from './palette_picker'; -export { FieldPicker, LensFieldIcon, TruncatedLabel } from './field_picker'; +export { FieldPicker, TruncatedLabel } from './field_picker'; export type { FieldOption, FieldOptionValue } from './field_picker'; export { ChangeIndexPattern, fieldContainsData } from './dataview_picker'; export { QueryInput, isQueryValid, validateQuery } from './query_input'; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 5e4ad5b920ed0..9e226dcdb1edc 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -89,7 +89,7 @@ export type IndexPatternField = FieldSpec & { * Map of fields which can be used, but may fail partially (ranked lower than others) */ partiallyApplicableFunctions?: Partial>; - timeSeriesMetricType?: 'histogram' | 'summary' | 'gauge' | 'counter'; + timeSeriesMetric?: 'histogram' | 'summary' | 'gauge' | 'counter'; timeSeriesRollup?: boolean; meta?: boolean; runtime?: boolean; @@ -571,7 +571,7 @@ export interface DatasourceDataPanelProps { showNoDataPopover: () => void; core: Pick< CoreStart, - 'http' | 'notifications' | 'uiSettings' | 'overlays' | 'theme' | 'application' + 'http' | 'notifications' | 'uiSettings' | 'overlays' | 'theme' | 'application' | 'docLinks' >; query: Query; dateRange: DateRange; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index c6c3752bfc724..f3d5ecde3eae8 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -18431,18 +18431,6 @@ "xpack.lens.datatable.suggestionLabel": "En tant que tableau", "xpack.lens.datatable.titleLabel": "Titre", "xpack.lens.datatable.visualizationName": "Tableau de données", - "xpack.lens.datatypes.boolean": "Booléen", - "xpack.lens.datatypes.counter": "Indicateur de compteur", - "xpack.lens.datatypes.date": "Date", - "xpack.lens.datatypes.gauge": "Indicateur de jauge", - "xpack.lens.datatypes.geoPoint": "Point géographique", - "xpack.lens.datatypes.geoShape": "Forme géographique", - "xpack.lens.datatypes.histogram": "Histogramme", - "xpack.lens.datatypes.ipAddress": "Adresse IP", - "xpack.lens.datatypes.murmur3": "murmur3", - "xpack.lens.datatypes.number": "Nombre", - "xpack.lens.datatypes.record": "Enregistrement", - "xpack.lens.datatypes.string": "Chaîne de texte", "xpack.lens.deleteLayerAriaLabel": "Supprimer le calque", "xpack.lens.dimensionContainer.close": "Fermer", "xpack.lens.dimensionContainer.closeConfiguration": "Fermer la configuration", @@ -18880,9 +18868,6 @@ "xpack.lens.indexPattern.timeShiftPlaceholder": "Saisissez des valeurs personnalisées (par ex. 8w)", "xpack.lens.indexPattern.useAsTopLevelAgg": "Agréger d'abord en fonction de cette dimension", "xpack.lens.indexPattern.useFieldExistenceSampling.advancedSettings": "Paramètres avancés", - "xpack.lens.indexPatterns.clearFiltersLabel": "Effacer le nom et saisissez les filtres", - "xpack.lens.indexPatterns.filterByNameLabel": "Rechercher les noms de champs", - "xpack.lens.indexPatterns.filterByTypeAriaLabel": "Filtrer par type", "xpack.lens.label.gauge.labelMajor.header": "Titre", "xpack.lens.label.gauge.labelMinor.header": "Sous-titre", "xpack.lens.label.header": "Étiquette", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 95aef8c8ef76a..e4d7993307a28 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -18414,18 +18414,6 @@ "xpack.lens.datatable.suggestionLabel": "表として", "xpack.lens.datatable.titleLabel": "タイトル", "xpack.lens.datatable.visualizationName": "データベース", - "xpack.lens.datatypes.boolean": "ブール", - "xpack.lens.datatypes.counter": "カウンターメトリック", - "xpack.lens.datatypes.date": "日付", - "xpack.lens.datatypes.gauge": "ゲージメトリック", - "xpack.lens.datatypes.geoPoint": "地理的な位置", - "xpack.lens.datatypes.geoShape": "地理的な形状", - "xpack.lens.datatypes.histogram": "ヒストグラム", - "xpack.lens.datatypes.ipAddress": "IP アドレス", - "xpack.lens.datatypes.murmur3": "murmur3", - "xpack.lens.datatypes.number": "数字", - "xpack.lens.datatypes.record": "レコード", - "xpack.lens.datatypes.string": "テキスト文字列", "xpack.lens.deleteLayerAriaLabel": "レイヤーを削除", "xpack.lens.dimensionContainer.close": "閉じる", "xpack.lens.dimensionContainer.closeConfiguration": "構成を閉じる", @@ -18863,9 +18851,6 @@ "xpack.lens.indexPattern.timeShiftPlaceholder": "カスタム値を入力(例:8w)", "xpack.lens.indexPattern.useAsTopLevelAgg": "最初にこのディメンションで集約", "xpack.lens.indexPattern.useFieldExistenceSampling.advancedSettings": "高度な設定", - "xpack.lens.indexPatterns.clearFiltersLabel": "名前とタイプフィルターを消去", - "xpack.lens.indexPatterns.filterByNameLabel": "検索フィールド名", - "xpack.lens.indexPatterns.filterByTypeAriaLabel": "タイプでフィルタリング", "xpack.lens.label.gauge.labelMajor.header": "タイトル", "xpack.lens.label.gauge.labelMinor.header": "サブタイトル", "xpack.lens.label.header": "ラベル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8acf63ed7d3e8..79d9ca5df0b89 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -18438,18 +18438,6 @@ "xpack.lens.datatable.suggestionLabel": "作为表", "xpack.lens.datatable.titleLabel": "标题", "xpack.lens.datatable.visualizationName": "数据表", - "xpack.lens.datatypes.boolean": "布尔型", - "xpack.lens.datatypes.counter": "计数器指标", - "xpack.lens.datatypes.date": "日期", - "xpack.lens.datatypes.gauge": "仪表盘指标", - "xpack.lens.datatypes.geoPoint": "地理点", - "xpack.lens.datatypes.geoShape": "地理形状", - "xpack.lens.datatypes.histogram": "直方图", - "xpack.lens.datatypes.ipAddress": "IP 地址", - "xpack.lens.datatypes.murmur3": "murmur3", - "xpack.lens.datatypes.number": "数字", - "xpack.lens.datatypes.record": "记录", - "xpack.lens.datatypes.string": "文本字符串", "xpack.lens.deleteLayerAriaLabel": "删除图层", "xpack.lens.dimensionContainer.close": "关闭", "xpack.lens.dimensionContainer.closeConfiguration": "关闭配置", @@ -18887,9 +18875,6 @@ "xpack.lens.indexPattern.timeShiftPlaceholder": "键入定制值(如 8w)", "xpack.lens.indexPattern.useAsTopLevelAgg": "首先按此维度聚合", "xpack.lens.indexPattern.useFieldExistenceSampling.advancedSettings": "高级设置", - "xpack.lens.indexPatterns.clearFiltersLabel": "清除名称和类型筛选", - "xpack.lens.indexPatterns.filterByNameLabel": "搜索字段名称", - "xpack.lens.indexPatterns.filterByTypeAriaLabel": "按类型筛选", "xpack.lens.label.gauge.labelMajor.header": "标题", "xpack.lens.label.gauge.labelMinor.header": "子标题", "xpack.lens.label.header": "标签", diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index a2d0faa34dc99..49785c62e7310 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -38,7 +38,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * Clicks the index pattern filters toggle. */ async toggleIndexPatternFiltersPopover() { - await testSubjects.click('lnsIndexPatternFiltersToggle'); + await testSubjects.click('lnsIndexPatternFieldTypeFilterToggle'); }, async findAllFields() { From fe9ea67178c6f2165e52ca848de5e73f5fe8e57a Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Tue, 20 Dec 2022 14:04:49 +0100 Subject: [PATCH 51/55] adds time_series bucket aggregation (#147505) --- .../data/common/search/aggs/agg_types.ts | 5 +- .../common/search/aggs/aggs_service.test.ts | 2 + .../search/aggs/buckets/bucket_agg_types.ts | 1 + .../data/common/search/aggs/buckets/index.ts | 2 + .../common/search/aggs/buckets/time_series.ts | 30 ++++++++ .../aggs/buckets/time_series_fn.test.ts | 34 +++++++++ .../search/aggs/buckets/time_series_fn.ts | 75 +++++++++++++++++++ src/plugins/data/common/search/aggs/types.ts | 5 ++ .../public/search/aggs/aggs_service.test.ts | 4 +- 9 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 src/plugins/data/common/search/aggs/buckets/time_series.ts create mode 100644 src/plugins/data/common/search/aggs/buckets/time_series_fn.test.ts create mode 100644 src/plugins/data/common/search/aggs/buckets/time_series_fn.ts diff --git a/src/plugins/data/common/search/aggs/agg_types.ts b/src/plugins/data/common/search/aggs/agg_types.ts index ba000c6f6ba78..5e8343ba417b4 100644 --- a/src/plugins/data/common/search/aggs/agg_types.ts +++ b/src/plugins/data/common/search/aggs/agg_types.ts @@ -9,9 +9,8 @@ import { FieldFormatsStartCommon } from '@kbn/field-formats-plugin/common'; import * as buckets from './buckets'; -import * as metrics from './metrics'; - import { BUCKET_TYPES, CalculateBoundsFn } from './buckets'; +import * as metrics from './metrics'; import { METRIC_TYPES } from './metrics'; export interface AggTypesDependencies { @@ -71,6 +70,7 @@ export const getAggTypes = () => ({ { name: BUCKET_TYPES.GEOTILE_GRID, fn: buckets.getGeoTitleBucketAgg }, { name: BUCKET_TYPES.SAMPLER, fn: buckets.getSamplerBucketAgg }, { name: BUCKET_TYPES.DIVERSIFIED_SAMPLER, fn: buckets.getDiversifiedSamplerBucketAgg }, + { name: BUCKET_TYPES.TIME_SERIES, fn: buckets.getTimeSeriesBucketAgg }, ], }); @@ -88,6 +88,7 @@ export const getAggTypesFunctions = () => [ buckets.aggHistogram, buckets.aggDateHistogram, buckets.aggTerms, + buckets.aggTimeSeries, buckets.aggMultiTerms, buckets.aggRareTerms, buckets.aggSampler, diff --git a/src/plugins/data/common/search/aggs/aggs_service.test.ts b/src/plugins/data/common/search/aggs/aggs_service.test.ts index e654234e27803..c12b2d93b029d 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -69,6 +69,7 @@ describe('Aggs service', () => { "geotile_grid", "sampler", "diversified_sampler", + "time_series", "foo", ] `); @@ -124,6 +125,7 @@ describe('Aggs service', () => { "geotile_grid", "sampler", "diversified_sampler", + "time_series", ] `); expect(bStart.types.getAll().metrics.map((t) => t.name)).toMatchInlineSnapshot(` diff --git a/src/plugins/data/common/search/aggs/buckets/bucket_agg_types.ts b/src/plugins/data/common/search/aggs/buckets/bucket_agg_types.ts index fcfbb432e3055..fa34c7f6535c1 100644 --- a/src/plugins/data/common/search/aggs/buckets/bucket_agg_types.ts +++ b/src/plugins/data/common/search/aggs/buckets/bucket_agg_types.ts @@ -23,4 +23,5 @@ export enum BUCKET_TYPES { DATE_HISTOGRAM = 'date_histogram', SAMPLER = 'sampler', DIVERSIFIED_SAMPLER = 'diversified_sampler', + TIME_SERIES = 'time_series', } diff --git a/src/plugins/data/common/search/aggs/buckets/index.ts b/src/plugins/data/common/search/aggs/buckets/index.ts index 000dcd5382b56..1f673f96199fd 100644 --- a/src/plugins/data/common/search/aggs/buckets/index.ts +++ b/src/plugins/data/common/search/aggs/buckets/index.ts @@ -48,4 +48,6 @@ export * from './sampler_fn'; export * from './sampler'; export * from './diversified_sampler_fn'; export * from './diversified_sampler'; +export * from './time_series'; +export * from './time_series_fn'; export { SHARD_DELAY_AGG_NAME } from './shard_delay'; diff --git a/src/plugins/data/common/search/aggs/buckets/time_series.ts b/src/plugins/data/common/search/aggs/buckets/time_series.ts new file mode 100644 index 0000000000000..643135c4ae5e7 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/time_series.ts @@ -0,0 +1,30 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +import { BucketAggType } from './bucket_agg_type'; +import { BUCKET_TYPES } from './bucket_agg_types'; +import { aggTimeSeriesFnName } from './time_series_fn'; +import { BaseAggParams } from '../types'; + +export { termsAggFilter } from './_terms_order_helper'; + +const timeSeriesTitle = i18n.translate('data.search.aggs.buckets.timeSeriesTitle', { + defaultMessage: 'Time Series', +}); + +export type AggParamsTimeSeries = BaseAggParams; + +export const getTimeSeriesBucketAgg = () => + new BucketAggType({ + name: BUCKET_TYPES.TIME_SERIES, + expressionName: aggTimeSeriesFnName, + title: timeSeriesTitle, + params: [], + }); diff --git a/src/plugins/data/common/search/aggs/buckets/time_series_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/time_series_fn.test.ts new file mode 100644 index 0000000000000..86fa9cdc07736 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/time_series_fn.test.ts @@ -0,0 +1,34 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggTimeSeries } from './time_series_fn'; + +describe('agg_expression_functions', () => { + describe('timeSeries', () => { + const fn = functionWrapper(aggTimeSeries()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({}); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "customLabel": undefined, + }, + "schema": undefined, + "type": "time_series", + }, + } + `); + }); + }); +}); diff --git a/src/plugins/data/common/search/aggs/buckets/time_series_fn.ts b/src/plugins/data/common/search/aggs/buckets/time_series_fn.ts new file mode 100644 index 0000000000000..6870f68a97d65 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/time_series_fn.ts @@ -0,0 +1,75 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; +import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '..'; + +export const aggTimeSeriesFnName = 'aggTimeSeries'; + +type Input = any; +type Output = AggExpressionType; +type AggArgs = AggExpressionFunctionArgs; + +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggTimeSeriesFnName, + Input, + AggArgs, + Output +>; + +export const aggTimeSeries = (): FunctionDefinition => ({ + name: aggTimeSeriesFnName, + help: i18n.translate('data.search.aggs.function.buckets.timeSeries.help', { + defaultMessage: 'Generates a serialized agg config for a Time Series agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.timeSeries.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.timeSeries.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.timeSeries.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: BUCKET_TYPES.TIME_SERIES, + params: { + ...rest, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index 1936efa8d328d..b7105baa2bd99 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -8,6 +8,7 @@ import { Assign } from '@kbn/utility-types'; import type { DataView } from '@kbn/data-views-plugin/common'; +import { aggTimeSeries } from './buckets/time_series_fn'; import { aggAvg, aggBucketAvg, @@ -110,6 +111,7 @@ import { AggParamsMovingAvgSerialized, AggParamsSerialDiffSerialized, AggParamsTopHitSerialized, + AggParamsTimeSeries, } from '.'; import { AggParamsSampler } from './buckets/sampler'; import { AggParamsDiversifiedSampler } from './buckets/diversified_sampler'; @@ -183,6 +185,7 @@ interface SerializedAggParamsMapping { [BUCKET_TYPES.HISTOGRAM]: AggParamsHistogram; [BUCKET_TYPES.DATE_HISTOGRAM]: AggParamsDateHistogram; [BUCKET_TYPES.TERMS]: AggParamsTermsSerialized; + [BUCKET_TYPES.TIME_SERIES]: AggParamsTimeSeries; [BUCKET_TYPES.MULTI_TERMS]: AggParamsMultiTermsSerialized; [BUCKET_TYPES.RARE_TERMS]: AggParamsRareTerms; [BUCKET_TYPES.SAMPLER]: AggParamsSampler; @@ -229,6 +232,7 @@ export interface AggParamsMapping { [BUCKET_TYPES.HISTOGRAM]: AggParamsHistogram; [BUCKET_TYPES.DATE_HISTOGRAM]: AggParamsDateHistogram; [BUCKET_TYPES.TERMS]: AggParamsTerms; + [BUCKET_TYPES.TIME_SERIES]: AggParamsTimeSeries; [BUCKET_TYPES.MULTI_TERMS]: AggParamsMultiTerms; [BUCKET_TYPES.RARE_TERMS]: AggParamsRareTerms; [BUCKET_TYPES.SAMPLER]: AggParamsSampler; @@ -276,6 +280,7 @@ export interface AggFunctionsMapping { aggHistogram: ReturnType; aggDateHistogram: ReturnType; aggTerms: ReturnType; + aggTimeSeries: ReturnType; aggMultiTerms: ReturnType; aggRareTerms: ReturnType; aggAvg: ReturnType; diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index e348779d6190d..9742314478704 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -52,7 +52,7 @@ describe('AggsService - public', () => { test('registers default agg types', () => { service.setup(setupDeps); const start = service.start(startDeps); - expect(start.types.getAll().buckets.length).toBe(16); + expect(start.types.getAll().buckets.length).toBe(17); expect(start.types.getAll().metrics.length).toBe(27); }); @@ -68,7 +68,7 @@ describe('AggsService - public', () => { ); const start = service.start(startDeps); - expect(start.types.getAll().buckets.length).toBe(17); + expect(start.types.getAll().buckets.length).toBe(18); expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true); expect(start.types.getAll().metrics.length).toBe(28); expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true); From 9a5df11ea07d9512491b141368f7a31e7abaf2e7 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 20 Dec 2022 07:23:40 -0600 Subject: [PATCH 52/55] [data views] allow fields param to be passed to field caps request (#147666) ## Summary Adds new optional `fields` argument to the data view fields for wildcard api that allows the consumer to request only specific fields as opposed to always getting all fields. Co-authored-by: Steph Milovic Co-authored-by: Julia Rechkunova --- .../data_views/common/data_views/data_views.ts | 6 +----- src/plugins/data_views/common/types.ts | 1 + .../public/data_views/data_views_api_client.ts | 13 +++++++++++-- .../server/fetcher/index_patterns_fetcher.ts | 2 ++ .../data_views/server/fetcher/lib/es_api.test.js | 2 +- src/plugins/data_views/server/fetcher/lib/es_api.ts | 4 +++- .../field_capabilities/field_capabilities.test.js | 1 + .../lib/field_capabilities/field_capabilities.ts | 11 ++++++++++- .../data_views/server/index_patterns_api_client.ts | 4 +++- src/plugins/data_views/server/routes/fields_for.ts | 3 +++ .../fields_for_wildcard_route/response.js | 10 ++++++++++ 11 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts index 2a5f8b8171bac..c497b9a7089e1 100644 --- a/src/plugins/data_views/common/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -497,12 +497,8 @@ export class DataViewsService { getFieldsForWildcard = async (options: GetFieldsOptions): Promise => { const metaFields = await this.config.get(META_FIELDS); const { fields } = await this.apiClient.getFieldsForWildcard({ - pattern: options.pattern, + ...options, metaFields, - type: options.type, - rollupIndex: options.rollupIndex, - allowNoIndex: options.allowNoIndex, - indexFilter: options.indexFilter, }); return fields; }; diff --git a/src/plugins/data_views/common/types.ts b/src/plugins/data_views/common/types.ts index b622280020060..4e2d308692afe 100644 --- a/src/plugins/data_views/common/types.ts +++ b/src/plugins/data_views/common/types.ts @@ -315,6 +315,7 @@ export interface GetFieldsOptions { allowNoIndex?: boolean; indexFilter?: QueryDslQueryContainer; includeUnmapped?: boolean; + fields?: string[]; } /** diff --git a/src/plugins/data_views/public/data_views/data_views_api_client.ts b/src/plugins/data_views/public/data_views/data_views_api_client.ts index 87fa8abe8f260..2b5e66d36f3a5 100644 --- a/src/plugins/data_views/public/data_views/data_views_api_client.ts +++ b/src/plugins/data_views/public/data_views/data_views_api_client.ts @@ -49,8 +49,16 @@ export class DataViewsApiClient implements IDataViewsApiClient { * @param options options for fields request */ getFieldsForWildcard(options: GetFieldsOptions) { - const { pattern, metaFields, type, rollupIndex, allowNoIndex, indexFilter, includeUnmapped } = - options; + const { + pattern, + metaFields, + type, + rollupIndex, + allowNoIndex, + indexFilter, + includeUnmapped, + fields, + } = options; return this._request( this._getUrl(['_fields_for_wildcard']), { @@ -60,6 +68,7 @@ export class DataViewsApiClient implements IDataViewsApiClient { rollup_index: rollupIndex, allow_no_index: allowNoIndex, include_unmapped: includeUnmapped, + fields, }, indexFilter ? JSON.stringify({ index_filter: indexFilter }) : undefined ).then((response) => { diff --git a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts index 406581981b3e4..2d4b0d00c3ffd 100644 --- a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts @@ -61,6 +61,7 @@ export class IndexPatternsFetcher { type?: string; rollupIndex?: string; indexFilter?: QueryDslQueryContainer; + fields?: string[]; }): Promise<{ fields: FieldDescriptor[]; indices: string[] }> { const { pattern, metaFields = [], fieldCapsOptions, type, rollupIndex, indexFilter } = options; const patternList = Array.isArray(pattern) ? pattern : pattern.split(','); @@ -81,6 +82,7 @@ export class IndexPatternsFetcher { include_unmapped: fieldCapsOptions?.includeUnmapped, }, indexFilter, + fields: options.fields || ['*'], }); if (type === 'rollup' && rollupIndex) { const rollupFields: FieldDescriptor[] = []; diff --git a/src/plugins/data_views/server/fetcher/lib/es_api.test.js b/src/plugins/data_views/server/fetcher/lib/es_api.test.js index 6054b4bd9d12c..05baac5d9bcdb 100644 --- a/src/plugins/data_views/server/fetcher/lib/es_api.test.js +++ b/src/plugins/data_views/server/fetcher/lib/es_api.test.js @@ -161,7 +161,7 @@ describe('server/index_patterns/service/lib/es_api', () => { sinon.assert.calledOnce(fieldCaps); const passedOpts = fieldCaps.args[0][0]; - expect(passedOpts).toHaveProperty('fields', '*'); + expect(passedOpts).toHaveProperty('fields', ['*']); expect(passedOpts).toHaveProperty('ignore_unavailable', true); expect(passedOpts).toHaveProperty('allow_no_indices', false); }); diff --git a/src/plugins/data_views/server/fetcher/lib/es_api.ts b/src/plugins/data_views/server/fetcher/lib/es_api.ts index 1c5c4fdb6e86c..988e4a4ec28c8 100644 --- a/src/plugins/data_views/server/fetcher/lib/es_api.ts +++ b/src/plugins/data_views/server/fetcher/lib/es_api.ts @@ -44,6 +44,7 @@ interface FieldCapsApiParams { indices: string[] | string; fieldCapsOptions?: { allow_no_indices: boolean; include_unmapped?: boolean }; indexFilter?: QueryDslQueryContainer; + fields?: string[]; } /** @@ -67,12 +68,13 @@ export async function callFieldCapsApi(params: FieldCapsApiParams) { allow_no_indices: false, include_unmapped: false, }, + fields = ['*'], } = params; try { return await callCluster.fieldCaps( { index: indices, - fields: '*', + fields, ignore_unavailable: true, index_filter: indexFilter, ...fieldCapsOptions, diff --git a/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.test.js b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.test.js index c2677ef37a2b0..1b3be374bbd7c 100644 --- a/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.test.js +++ b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.test.js @@ -36,6 +36,7 @@ describe('index_patterns/field_capabilities/field_capabilities', () => { indices: undefined, fieldCapsOptions: undefined, indexFilter: undefined, + fields: undefined, ...args, }); diff --git a/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts index 2867dd34aea0a..4ec808c756873 100644 --- a/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts +++ b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts @@ -21,6 +21,7 @@ interface FieldCapabilitiesParams { metaFields: string[]; fieldCapsOptions?: { allow_no_indices: boolean; include_unmapped?: boolean }; indexFilter?: QueryDslQueryContainer; + fields?: string[]; } /** @@ -34,12 +35,20 @@ interface FieldCapabilitiesParams { * @return {Promise<{ fields: Array, indices: Array>}>} */ export async function getFieldCapabilities(params: FieldCapabilitiesParams) { - const { callCluster, indices = [], fieldCapsOptions, indexFilter, metaFields = [] } = params; + const { + callCluster, + indices = [], + fieldCapsOptions, + indexFilter, + metaFields = [], + fields, + } = params; const esFieldCaps = await callFieldCapsApi({ callCluster, indices, fieldCapsOptions, indexFilter, + fields, }); const fieldsFromFieldCapsByName = keyBy(readFieldCapsResponse(esFieldCaps.body), 'name'); diff --git a/src/plugins/data_views/server/index_patterns_api_client.ts b/src/plugins/data_views/server/index_patterns_api_client.ts index b57352d7cacbd..f470e7f8ed7df 100644 --- a/src/plugins/data_views/server/index_patterns_api_client.ts +++ b/src/plugins/data_views/server/index_patterns_api_client.ts @@ -26,7 +26,8 @@ export class IndexPatternsApiServer implements IDataViewsApiClient { type, rollupIndex, allowNoIndex, - indexFilter: indexFilter, + indexFilter, + fields, }: GetFieldsOptions) { const indexPatterns = new IndexPatternsFetcher(this.esClient, allowNoIndex); return await indexPatterns @@ -36,6 +37,7 @@ export class IndexPatternsApiServer implements IDataViewsApiClient { type, rollupIndex, indexFilter, + fields, }) .catch((err) => { if ( diff --git a/src/plugins/data_views/server/routes/fields_for.ts b/src/plugins/data_views/server/routes/fields_for.ts index 69738373044a7..415077258966d 100644 --- a/src/plugins/data_views/server/routes/fields_for.ts +++ b/src/plugins/data_views/server/routes/fields_for.ts @@ -37,6 +37,7 @@ interface IQuery { rollup_index?: string; allow_no_index?: boolean; include_unmapped?: boolean; + fields?: string[]; } const validate: RouteValidatorFullConfig<{}, IQuery, IBody> = { @@ -49,6 +50,7 @@ const validate: RouteValidatorFullConfig<{}, IQuery, IBody> = { rollup_index: schema.maybe(schema.string()), allow_no_index: schema.maybe(schema.boolean()), include_unmapped: schema.maybe(schema.boolean()), + fields: schema.maybe(schema.arrayOf(schema.string())), }), // not available to get request body: schema.maybe(schema.object({ index_filter: schema.any() })), @@ -86,6 +88,7 @@ const handler: RequestHandler<{}, IQuery, IBody> = async (context, request, resp includeUnmapped, }, indexFilter, + fields: request.query.fields, }); return response.ok({ diff --git a/test/api_integration/apis/data_views/fields_for_wildcard_route/response.js b/test/api_integration/apis/data_views/fields_for_wildcard_route/response.js index 2dfcdbb35cd71..9a7968d2c2531 100644 --- a/test/api_integration/apis/data_views/fields_for_wildcard_route/response.js +++ b/test/api_integration/apis/data_views/fields_for_wildcard_route/response.js @@ -90,6 +90,16 @@ export default function ({ getService }) { .then(ensureFieldsAreSorted); }); + it('returns a single field as requested', async () => { + await supertest + .get('/api/index_patterns/_fields_for_wildcard') + .query({ pattern: 'basic_index', fields: JSON.stringify(['bar']) }) + .expect(200, { + fields: [testFields[0]], + indices: ['basic_index'], + }); + }); + it('always returns a field for all passed meta fields', async () => { await supertest .get('/api/index_patterns/_fields_for_wildcard') From 6b29787e6fb361358117c0accf7a0ef22c278fa4 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 20 Dec 2022 07:24:45 -0600 Subject: [PATCH 53/55] [data view editor] Revalidate index pattern when 'allow hidden' changes (#147807) ## Summary Steps to reproduce problem - 1) Open create data view flyout 2) Enter `.kibana` as index pattern - no indices will match, the field will display an error 3) Click on 'Advanced Options' 4) Click 'Allow Hidden' - the index pattern will still show an error This PR fixes the index pattern error state after step four. It will revalidate the index pattern field and load the timestamp field list again. --- .../advanced_params_content/advanced_params_content.tsx | 3 +++ .../public/components/data_view_editor_flyout_content.tsx | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/plugins/data_view_editor/public/components/advanced_params_content/advanced_params_content.tsx b/src/plugins/data_view_editor/public/components/advanced_params_content/advanced_params_content.tsx index 3e34d0729f28d..dcb8e2c1e1d80 100644 --- a/src/plugins/data_view_editor/public/components/advanced_params_content/advanced_params_content.tsx +++ b/src/plugins/data_view_editor/public/components/advanced_params_content/advanced_params_content.tsx @@ -29,11 +29,13 @@ const customIndexPatternIdLabel = i18n.translate( interface AdvancedParamsContentProps { disableAllowHidden: boolean; disableId: boolean; + onAllowHiddenChange?: (value: boolean) => void; } export const AdvancedParamsContent = ({ disableAllowHidden, disableId, + onAllowHiddenChange, }: AdvancedParamsContentProps) => ( @@ -48,6 +50,7 @@ export const AdvancedParamsContent = ({ disabled: disableAllowHidden, }, }} + onChange={onAllowHiddenChange} /> diff --git a/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx b/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx index d046ad3bb2990..42426ccb99d92 100644 --- a/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx +++ b/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx @@ -281,6 +281,9 @@ const IndexPatternEditorFlyoutContentComponent = ({ { + form.getFields().title.validate(); + }} />