From 91a5b17cfd058ecab180094b29900b9c34e50f6a Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 5 Mar 2020 11:29:44 -0700 Subject: [PATCH 01/12] [SIEM] [Case] All cases page design updates (#59248) --- .../components/filter_popover/index.tsx | 119 +++++++++ .../__snapshots__/utility_bar.test.tsx.snap | 0 .../utility_bar_action.test.tsx.snap | 0 .../utility_bar_group.test.tsx.snap | 0 .../utility_bar_section.test.tsx.snap | 0 .../utility_bar_text.test.tsx.snap | 0 .../utility_bar/index.ts | 0 .../utility_bar/styles.tsx | 0 .../utility_bar/utility_bar.test.tsx | 2 +- .../utility_bar/utility_bar.tsx | 0 .../utility_bar/utility_bar_action.test.tsx | 2 +- .../utility_bar/utility_bar_action.tsx | 2 +- .../utility_bar/utility_bar_group.test.tsx | 2 +- .../utility_bar/utility_bar_group.tsx | 0 .../utility_bar/utility_bar_section.test.tsx | 2 +- .../utility_bar/utility_bar_section.tsx | 0 .../utility_bar/utility_bar_text.test.tsx | 2 +- .../utility_bar/utility_bar_text.tsx | 0 .../siem/public/containers/case/api.ts | 8 +- .../siem/public/containers/case/constants.ts | 1 + .../siem/public/containers/case/types.ts | 2 +- .../public/containers/case/use_get_cases.tsx | 200 +++++++++++---- .../containers/case/use_update_case.tsx | 2 +- .../plugins/siem/public/pages/case/case.tsx | 16 -- .../components/all_cases/__mock__/index.tsx | 9 +- .../case/components/all_cases/actions.tsx | 60 +++++ .../case/components/all_cases/columns.tsx | 101 +++++--- .../case/components/all_cases/index.test.tsx | 36 ++- .../pages/case/components/all_cases/index.tsx | 229 ++++++++++++++---- .../components/all_cases/table_filters.tsx | 66 +++-- .../case/components/all_cases/translations.ts | 55 +++-- .../case/components/bulk_actions/index.tsx | 72 ++++++ .../components/bulk_actions/translations.ts | 28 +++ .../components/open_closed_stats/index.tsx | 38 +++ .../siem/public/pages/case/translations.ts | 19 +- .../components/activity_monitor/index.tsx | 2 +- .../signals/signals_utility_bar/index.tsx | 2 +- .../detection_engine/rules/all/index.tsx | 2 +- 38 files changed, 863 insertions(+), 216 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/__snapshots__/utility_bar.test.tsx.snap (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/index.ts (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/styles.tsx (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar.test.tsx (98%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar.tsx (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_action.test.tsx (95%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_action.tsx (97%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_group.test.tsx (93%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_group.tsx (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_section.test.tsx (94%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_section.tsx (100%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_text.test.tsx (92%) rename x-pack/legacy/plugins/siem/public/components/{detection_engine => }/utility_bar/utility_bar_text.tsx (100%) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx b/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx new file mode 100644 index 0000000000000..1d269dffeccf5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Dispatch, SetStateAction, useCallback, useState } from 'react'; +import { + EuiFilterButton, + EuiFilterSelectItem, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiPopover, + EuiText, +} from '@elastic/eui'; +import styled from 'styled-components'; + +interface FilterPopoverProps { + buttonLabel: string; + onSelectedOptionsChanged: Dispatch>; + options: string[]; + optionsEmptyLabel: string; + selectedOptions: string[]; +} + +const ScrollableDiv = styled.div` + max-height: 250px; + overflow: auto; +`; + +export const toggleSelectedGroup = ( + group: string, + selectedGroups: string[], + setSelectedGroups: Dispatch> +): void => { + const selectedGroupIndex = selectedGroups.indexOf(group); + const updatedSelectedGroups = [...selectedGroups]; + if (selectedGroupIndex >= 0) { + updatedSelectedGroups.splice(selectedGroupIndex, 1); + } else { + updatedSelectedGroups.push(group); + } + return setSelectedGroups(updatedSelectedGroups); +}; + +/** + * Popover for selecting a field to filter on + * + * @param buttonLabel label on dropdwon button + * @param onSelectedOptionsChanged change listener to be notified when option selection changes + * @param options to display for filtering + * @param optionsEmptyLabel shows when options empty + * @param selectedOptions manage state of selectedOptions + */ +export const FilterPopoverComponent = ({ + buttonLabel, + onSelectedOptionsChanged, + options, + optionsEmptyLabel, + selectedOptions, +}: FilterPopoverProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const setIsPopoverOpenCb = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); + const toggleSelectedGroupCb = useCallback( + option => toggleSelectedGroup(option, selectedOptions, onSelectedOptionsChanged), + [selectedOptions, onSelectedOptionsChanged] + ); + + return ( + 0} + numActiveFilters={selectedOptions.length} + > + {buttonLabel} + + } + isOpen={isPopoverOpen} + closePopover={setIsPopoverOpenCb} + panelPaddingSize="none" + > + + {options.map((option, index) => ( + + {option} + + ))} + + {options.length === 0 && ( + + + + {optionsEmptyLabel} + + + + )} + + ); +}; + +FilterPopoverComponent.displayName = 'FilterPopoverComponent'; + +export const FilterPopover = React.memo(FilterPopoverComponent); + +FilterPopover.displayName = 'FilterPopover'; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap rename to x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap rename to x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap rename to x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap rename to x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap rename to x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/index.ts b/x-pack/legacy/plugins/siem/public/components/utility_bar/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/index.ts rename to x-pack/legacy/plugins/siem/public/components/utility_bar/index.ts diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/styles.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/styles.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/styles.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/styles.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.test.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.test.tsx index eae0fc4ff422b..5fd010362be10 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.test.tsx @@ -8,7 +8,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../mock'; +import { TestProviders } from '../../mock'; import { UtilityBar, UtilityBarAction, diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx similarity index 95% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx index 2a8a71955a986..09c62773fddd1 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx @@ -7,7 +7,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../mock'; +import { TestProviders } from '../../mock'; import { UtilityBarAction } from './index'; describe('UtilityBarAction', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx index 4e850a0a11957..d3e2be0e8f816 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx +++ b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx @@ -7,7 +7,7 @@ import { EuiPopover } from '@elastic/eui'; import React, { useCallback, useState } from 'react'; -import { LinkIcon, LinkIconProps } from '../../link_icon'; +import { LinkIcon, LinkIconProps } from '../link_icon'; import { BarAction } from './styles'; const Popover = React.memo( diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx similarity index 93% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx index e18e7d5e0b524..8e184e5aaec30 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../mock'; +import { TestProviders } from '../../mock'; import { UtilityBarGroup, UtilityBarText } from './index'; describe('UtilityBarGroup', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx similarity index 94% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx index f849fa4b4ee46..c6037c75670eb 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../mock'; +import { TestProviders } from '../../mock'; import { UtilityBarGroup, UtilityBarSection, UtilityBarText } from './index'; describe('UtilityBarSection', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx similarity index 92% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx index 230dd80b1a86b..fcfc2b6b0cefa 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx @@ -7,7 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../mock'; +import { TestProviders } from '../../mock'; import { UtilityBarText } from './index'; describe('UtilityBarText', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx b/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx rename to x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index f1d87ca58b44b..ff03a3799018c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -35,6 +35,7 @@ export const getCase = async (caseId: string, includeComments: boolean = true): export const getCases = async ({ filterOptions = { search: '', + state: 'open', tags: [], }, queryParams = { @@ -44,7 +45,12 @@ export const getCases = async ({ sortOrder: 'desc', }, }: FetchCasesProps): Promise => { - const tags = [...(filterOptions.tags?.map(t => `case-workflow.attributes.tags: ${t}`) ?? [])]; + const stateFilter = `case-workflow.attributes.state: ${filterOptions.state}`; + const tags = [ + ...(filterOptions.tags?.reduce((acc, t) => [...acc, `case-workflow.attributes.tags: ${t}`], [ + stateFilter, + ]) ?? [stateFilter]), + ]; const query = { ...queryParams, filter: tags.join(' AND '), diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts index 031ba1c128a24..ac62ba7b6f997 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -13,4 +13,5 @@ export const FETCH_SUCCESS = 'FETCH_SUCCESS'; export const POST_NEW_CASE = 'POST_NEW_CASE'; export const POST_NEW_COMMENT = 'POST_NEW_COMMENT'; export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS'; +export const UPDATE_TABLE_SELECTIONS = 'UPDATE_TABLE_SELECTIONS'; export const UPDATE_QUERY_PARAMS = 'UPDATE_QUERY_PARAMS'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index 75ed6f7c2366d..9cc9f519f3a62 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -71,6 +71,7 @@ export interface QueryParams { export interface FilterOptions { search: string; + state: string; tags: string[]; } @@ -89,7 +90,6 @@ export interface AllCases { } export enum SortFieldCase { createdAt = 'createdAt', - state = 'state', updatedAt = 'updatedAt', } diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 4037823ccfc94..e73b251477bf3 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -4,58 +4,87 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; +import { Dispatch, SetStateAction, useCallback, useEffect, useReducer, useState } from 'react'; import { isEqual } from 'lodash/fp'; -import { - DEFAULT_TABLE_ACTIVE_PAGE, - DEFAULT_TABLE_LIMIT, - FETCH_FAILURE, - FETCH_INIT, - FETCH_SUCCESS, - UPDATE_QUERY_PARAMS, - UPDATE_FILTER_OPTIONS, -} from './constants'; -import { AllCases, SortFieldCase, FilterOptions, QueryParams } from './types'; -import { getTypedPayload } from './utils'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants'; +import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case } from './types'; import { errorToToaster } from '../../components/ml/api/error_to_toaster'; import { useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; -import { getCases } from './api'; +import { UpdateByKey } from './use_update_case'; +import { getCases, updateCaseProperty } from './api'; export interface UseGetCasesState { + caseCount: CaseCount; data: AllCases; - isLoading: boolean; + filterOptions: FilterOptions; isError: boolean; + loading: string[]; queryParams: QueryParams; - filterOptions: FilterOptions; + selectedCases: Case[]; +} + +export interface CaseCount { + open: number; + closed: number; } -export interface Action { - type: string; - payload?: AllCases | Partial | FilterOptions; +export interface UpdateCase extends UpdateByKey { + caseId: string; + version: string; } + +export type Action = + | { type: 'FETCH_INIT'; payload: string } + | { type: 'FETCH_CASE_COUNT_SUCCESS'; payload: Partial } + | { type: 'FETCH_CASES_SUCCESS'; payload: AllCases } + | { type: 'FETCH_FAILURE'; payload: string } + | { type: 'FETCH_UPDATE_CASE_SUCCESS' } + | { type: 'UPDATE_FILTER_OPTIONS'; payload: FilterOptions } + | { type: 'UPDATE_QUERY_PARAMS'; payload: Partial } + | { type: 'UPDATE_TABLE_SELECTIONS'; payload: Case[] }; + const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesState => { switch (action.type) { - case FETCH_INIT: + case 'FETCH_INIT': return { ...state, - isLoading: true, isError: false, + loading: [...state.loading.filter(e => e !== action.payload), action.payload], + }; + case 'FETCH_UPDATE_CASE_SUCCESS': + return { + ...state, + loading: state.loading.filter(e => e !== 'caseUpdate'), + }; + case 'FETCH_CASE_COUNT_SUCCESS': + return { + ...state, + caseCount: { + ...state.caseCount, + ...action.payload, + }, + loading: state.loading.filter(e => e !== 'caseCount'), }; - case FETCH_SUCCESS: + case 'FETCH_CASES_SUCCESS': return { ...state, - isLoading: false, isError: false, - data: getTypedPayload(action.payload), + data: action.payload, + loading: state.loading.filter(e => e !== 'cases'), }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, - isLoading: false, isError: true, + loading: state.loading.filter(e => e !== action.payload), }; - case UPDATE_QUERY_PARAMS: + case 'UPDATE_FILTER_OPTIONS': + return { + ...state, + filterOptions: action.payload, + }; + case 'UPDATE_QUERY_PARAMS': return { ...state, queryParams: { @@ -63,10 +92,10 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS ...action.payload, }, }; - case UPDATE_FILTER_OPTIONS: + case 'UPDATE_TABLE_SELECTIONS': return { ...state, - filterOptions: getTypedPayload(action.payload), + selectedCases: action.payload, }; default: throw new Error(); @@ -74,51 +103,64 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS }; const initialData: AllCases = { + cases: [], page: 0, perPage: 0, total: 0, - cases: [], }; -export const useGetCases = (): [ - UseGetCasesState, - Dispatch>>, - Dispatch> -] => { +interface UseGetCases extends UseGetCasesState { + dispatchUpdateCaseProperty: Dispatch; + getCaseCount: Dispatch; + setFilters: Dispatch>; + setQueryParams: Dispatch>>; + setSelectedCases: Dispatch; +} +export const useGetCases = (): UseGetCases => { const [state, dispatch] = useReducer(dataFetchReducer, { - isLoading: false, - isError: false, + caseCount: { + open: 0, + closed: 0, + }, data: initialData, filterOptions: { search: '', + state: 'open', tags: [], }, + isError: false, + loading: [], queryParams: { page: DEFAULT_TABLE_ACTIVE_PAGE, perPage: DEFAULT_TABLE_LIMIT, sortField: SortFieldCase.createdAt, sortOrder: 'desc', }, + selectedCases: [], }); - const [queryParams, setQueryParams] = useState>(state.queryParams); - const [filterQuery, setFilters] = useState(state.filterOptions); const [, dispatchToaster] = useStateToaster(); + const [filterQuery, setFilters] = useState(state.filterOptions); + const [queryParams, setQueryParams] = useState>(state.queryParams); + + const setSelectedCases = useCallback((mySelectedCases: Case[]) => { + dispatch({ type: 'UPDATE_TABLE_SELECTIONS', payload: mySelectedCases }); + }, []); useEffect(() => { if (!isEqual(queryParams, state.queryParams)) { - dispatch({ type: UPDATE_QUERY_PARAMS, payload: queryParams }); + dispatch({ type: 'UPDATE_QUERY_PARAMS', payload: queryParams }); } }, [queryParams, state.queryParams]); useEffect(() => { if (!isEqual(filterQuery, state.filterOptions)) { - dispatch({ type: UPDATE_FILTER_OPTIONS, payload: filterQuery }); + dispatch({ type: 'UPDATE_FILTER_OPTIONS', payload: filterQuery }); } }, [filterQuery, state.filterOptions]); - useEffect(() => { + const fetchCases = useCallback(() => { let didCancel = false; const fetchData = async () => { - dispatch({ type: FETCH_INIT }); + dispatch({ type: 'FETCH_INIT', payload: 'cases' }); try { const response = await getCases({ filterOptions: state.filterOptions, @@ -126,14 +168,14 @@ export const useGetCases = (): [ }); if (!didCancel) { dispatch({ - type: FETCH_SUCCESS, + type: 'FETCH_CASES_SUCCESS', payload: response, }); } } catch (error) { if (!didCancel) { errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE', payload: 'cases' }); } } }; @@ -142,5 +184,73 @@ export const useGetCases = (): [ didCancel = true; }; }, [state.queryParams, state.filterOptions]); - return [state, setQueryParams, setFilters]; + useEffect(() => fetchCases(), [state.queryParams, state.filterOptions]); + + const getCaseCount = useCallback((caseState: keyof CaseCount) => { + let didCancel = false; + const fetchData = async () => { + dispatch({ type: 'FETCH_INIT', payload: 'caseCount' }); + try { + const response = await getCases({ + filterOptions: { search: '', state: caseState, tags: [] }, + }); + if (!didCancel) { + dispatch({ + type: 'FETCH_CASE_COUNT_SUCCESS', + payload: { [caseState]: response.total }, + }); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: 'FETCH_FAILURE', payload: 'caseCount' }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }, []); + + const dispatchUpdateCaseProperty = useCallback( + ({ updateKey, updateValue, caseId, version }: UpdateCase) => { + let didCancel = false; + const fetchData = async () => { + dispatch({ type: 'FETCH_INIT', payload: 'caseUpdate' }); + try { + await updateCaseProperty( + caseId, + { [updateKey]: updateValue }, + version ?? '' // saved object versions are typed as string | undefined, hope that's not true + ); + if (!didCancel) { + dispatch({ type: 'FETCH_UPDATE_CASE_SUCCESS' }); + fetchCases(); + getCaseCount('open'); + getCaseCount('closed'); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: 'FETCH_FAILURE', payload: 'caseUpdate' }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }, + [filterQuery, state.filterOptions] + ); + + return { + ...state, + dispatchUpdateCaseProperty, + getCaseCount, + setFilters, + setQueryParams, + setSelectedCases, + }; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index ebbb1e14dc237..f23be526fbeb7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -22,7 +22,7 @@ interface NewCaseState { updateKey: UpdateKey | null; } -interface UpdateByKey { +export interface UpdateByKey { updateKey: UpdateKey; updateValue: Case[UpdateKey]; } diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index 15a6d076f1009..9255dee461940 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -6,29 +6,13 @@ import React from 'react'; -import { EuiButton, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { CaseHeaderPage } from './components/case_header_page'; import { WrapperPage } from '../../components/wrapper_page'; import { AllCases } from './components/all_cases'; import { SpyRoute } from '../../utils/route/spy_routes'; -import * as i18n from './translations'; -import { getCreateCaseUrl, getConfigureCasesUrl } from '../../components/link_to'; export const CasesPage = React.memo(() => ( <> - - - - - {i18n.CREATE_TITLE} - - - - - - - diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx index 0169493773b74..a054d685399bc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx @@ -75,7 +75,12 @@ export const useGetCasesMockState: UseGetCasesState = { perPage: 5, total: 10, }, - isLoading: false, + caseCount: { + open: 0, + closed: 0, + }, + loading: [], + selectedCases: [], isError: false, queryParams: { page: 1, @@ -83,5 +88,5 @@ export const useGetCasesMockState: UseGetCasesState = { sortField: SortFieldCase.createdAt, sortOrder: 'desc', }, - filterOptions: { search: '', tags: [] }, + filterOptions: { search: '', tags: [], state: 'open' }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx new file mode 100644 index 0000000000000..5dad19b1e54d3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; +import { Dispatch } from 'react'; +import { Case } from '../../../../containers/case/types'; + +import * as i18n from './translations'; +import { UpdateCase } from '../../../../containers/case/use_get_cases'; + +interface GetActions { + caseStatus: string; + dispatchUpdate: Dispatch; +} + +export const getActions = ({ + caseStatus, + dispatchUpdate, +}: GetActions): Array> => [ + { + description: i18n.DELETE, + icon: 'trash', + name: i18n.DELETE, + // eslint-disable-next-line no-console + onClick: ({ caseId }: Case) => console.log('TO DO Delete case', caseId), + type: 'icon', + 'data-test-subj': 'action-delete', + }, + caseStatus === 'open' + ? { + description: i18n.CLOSE_CASE, + icon: 'magnet', + name: i18n.CLOSE_CASE, + onClick: (theCase: Case) => + dispatchUpdate({ + updateKey: 'state', + updateValue: 'closed', + caseId: theCase.caseId, + version: theCase.version, + }), + type: 'icon', + 'data-test-subj': 'action-close', + } + : { + description: i18n.REOPEN_CASE, + icon: 'magnet', + name: i18n.REOPEN_CASE, + onClick: (theCase: Case) => + dispatchUpdate({ + updateKey: 'state', + updateValue: 'open', + caseId: theCase.caseId, + version: theCase.version, + }), + type: 'icon', + 'data-test-subj': 'action-open', + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index 9c276d1b24da1..41a2bdf52d5a1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -4,7 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { EuiBadge, EuiTableFieldDataColumnType, EuiTableComputedColumnType } from '@elastic/eui'; +import { + EuiBadge, + EuiTableFieldDataColumnType, + EuiTableComputedColumnType, + EuiTableActionsColumnType, + EuiAvatar, +} from '@elastic/eui'; +import styled from 'styled-components'; +import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; import { getEmptyTagValue } from '../../../../components/empty_value'; import { Case } from '../../../../containers/case/types'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; @@ -12,17 +20,61 @@ import { CaseDetailsLink } from '../../../../components/links'; import { TruncatableText } from '../../../../components/truncatable_text'; import * as i18n from './translations'; -export type CasesColumns = EuiTableFieldDataColumnType | EuiTableComputedColumnType; +export type CasesColumns = + | EuiTableFieldDataColumnType + | EuiTableComputedColumnType + | EuiTableActionsColumnType; -const renderStringField = (field: string, dataTestSubj: string) => - field != null ? {field} : getEmptyTagValue(); +const MediumShadeText = styled.p` + color: ${({ theme }) => theme.eui.euiColorMediumShade}; +`; -export const getCasesColumns = (): CasesColumns[] => [ +const Spacer = styled.span` + margin-left: ${({ theme }) => theme.eui.paddingSizes.s}; +`; + +const TempNumberComponent = () => {1}; +TempNumberComponent.displayName = 'TempNumberComponent'; + +export const getCasesColumns = ( + actions: Array> +): CasesColumns[] => [ { name: i18n.NAME, render: (theCase: Case) => { if (theCase.caseId != null && theCase.title != null) { - return {theCase.title}; + const caseDetailsLinkComponent = ( + {theCase.title} + ); + return theCase.state === 'open' ? ( + caseDetailsLinkComponent + ) : ( + <> + + {caseDetailsLinkComponent} + {i18n.CLOSED} + + + ); + } + return getEmptyTagValue(); + }, + }, + { + field: 'createdBy', + name: i18n.REPORTER, + render: (createdBy: Case['createdBy']) => { + if (createdBy != null) { + return ( + <> + + {createdBy.username} + + ); } return getEmptyTagValue(); }, @@ -50,9 +102,16 @@ export const getCasesColumns = (): CasesColumns[] => [ }, truncateText: true, }, + { + align: 'right', + field: 'commentCount', // TO DO once we have commentCount returned in the API: https://github.com/elastic/kibana/issues/58525 + name: i18n.COMMENTS, + sortable: true, + render: TempNumberComponent, + }, { field: 'createdAt', - name: i18n.CREATED_AT, + name: i18n.OPENED_ON, sortable: true, render: (createdAt: Case['createdAt']) => { if (createdAt != null) { @@ -67,31 +126,7 @@ export const getCasesColumns = (): CasesColumns[] => [ }, }, { - field: 'createdBy.username', - name: i18n.REPORTER, - render: (createdBy: Case['createdBy']['username']) => - renderStringField(createdBy, `case-table-column-username`), - }, - { - field: 'updatedAt', - name: i18n.LAST_UPDATED, - sortable: true, - render: (updatedAt: Case['updatedAt']) => { - if (updatedAt != null) { - return ( - - ); - } - return getEmptyTagValue(); - }, - }, - { - field: 'state', - name: i18n.STATE, - sortable: true, - render: (state: Case['state']) => renderStringField(state, `case-table-column-state`), + name: 'Actions', + actions, }, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index 5a87cf53142f7..dd584f3f716b6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -13,13 +13,21 @@ import { useGetCasesMockState } from './__mock__'; import * as apiHook from '../../../../containers/case/use_get_cases'; describe('AllCases', () => { - const setQueryParams = jest.fn(); const setFilters = jest.fn(); + const setQueryParams = jest.fn(); + const setSelectedCases = jest.fn(); + const getCaseCount = jest.fn(); + const dispatchUpdateCaseProperty = jest.fn(); beforeEach(() => { jest.resetAllMocks(); - jest - .spyOn(apiHook, 'useGetCases') - .mockReturnValue([useGetCasesMockState, setQueryParams, setFilters]); + jest.spyOn(apiHook, 'useGetCases').mockReturnValue({ + ...useGetCasesMockState, + dispatchUpdateCaseProperty, + getCaseCount, + setFilters, + setQueryParams, + setSelectedCases, + }); moment.tz.setDefault('UTC'); }); it('should render AllCases', () => { @@ -40,12 +48,6 @@ describe('AllCases', () => { .first() .text() ).toEqual(useGetCasesMockState.data.cases[0].title); - expect( - wrapper - .find(`[data-test-subj="case-table-column-state"]`) - .first() - .text() - ).toEqual(useGetCasesMockState.data.cases[0].state); expect( wrapper .find(`span[data-test-subj="case-table-column-tags-0"]`) @@ -54,7 +56,7 @@ describe('AllCases', () => { ).toEqual(useGetCasesMockState.data.cases[0].tags[0]); expect( wrapper - .find(`[data-test-subj="case-table-column-username"]`) + .find(`[data-test-subj="case-table-column-createdBy"]`) .first() .text() ).toEqual(useGetCasesMockState.data.cases[0].createdBy.username); @@ -64,13 +66,6 @@ describe('AllCases', () => { .first() .prop('value') ).toEqual(useGetCasesMockState.data.cases[0].createdAt); - expect( - wrapper - .find(`[data-test-subj="case-table-column-updatedAt"]`) - .first() - .prop('value') - ).toEqual(useGetCasesMockState.data.cases[0].updatedAt); - expect( wrapper .find(`[data-test-subj="case-table-case-count"]`) @@ -85,12 +80,13 @@ describe('AllCases', () => { ); wrapper - .find('[data-test-subj="tableHeaderCell_state_5"] [data-test-subj="tableHeaderSortButton"]') + .find('[data-test-subj="tableHeaderSortButton"]') + .first() .simulate('click'); expect(setQueryParams).toBeCalledWith({ page: 1, perPage: 5, - sortField: 'state', + sortField: 'createdAt', sortOrder: 'asc', }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 3253a036c2990..484d9051ee43f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -8,45 +8,85 @@ import React, { useCallback, useMemo } from 'react'; import { EuiBasicTable, EuiButton, + EuiButtonIcon, + EuiContextMenuPanel, EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, EuiLoadingContent, + EuiProgress, EuiTableSortingType, } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; +import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; +import styled, { css } from 'styled-components'; import * as i18n from './translations'; import { getCasesColumns } from './columns'; -import { SortFieldCase, Case, FilterOptions } from '../../../../containers/case/types'; +import { Case, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; import { useGetCases } from '../../../../containers/case/use_get_cases'; import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; import { Panel } from '../../../../components/panel'; -import { HeaderSection } from '../../../../components/header_section'; import { CasesTableFilters } from './table_filters'; import { UtilityBar, + UtilityBarAction, UtilityBarGroup, UtilityBarSection, UtilityBarText, -} from '../../../../components/detection_engine/utility_bar'; -import { getCreateCaseUrl } from '../../../../components/link_to'; +} from '../../../../components/utility_bar'; +import { getConfigureCasesUrl, getCreateCaseUrl } from '../../../../components/link_to'; +import { getBulkItems } from '../bulk_actions'; +import { CaseHeaderPage } from '../case_header_page'; +import { OpenClosedStats } from '../open_closed_stats'; +import { getActions } from './actions'; + +const Div = styled.div` + margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; +`; +const FlexItemDivider = styled(EuiFlexItem)` + ${({ theme }) => css` + .euiFlexGroup--gutterMedium > &.euiFlexItem { + border-right: ${theme.eui.euiBorderThin}; + padding-right: ${theme.eui.euiSize}; + margin-right: ${theme.eui.euiSize}; + } + `} +`; + +const ProgressLoader = styled(EuiProgress)` + ${({ theme }) => css` + .euiFlexGroup--gutterMedium > &.euiFlexItem { + top: 2px; + border-radius: ${theme.eui.euiBorderRadius}; + z-index: ${theme.eui.euiZHeader}; + } + `} +`; + const getSortField = (field: string): SortFieldCase => { if (field === SortFieldCase.createdAt) { return SortFieldCase.createdAt; - } else if (field === SortFieldCase.state) { - return SortFieldCase.state; } else if (field === SortFieldCase.updatedAt) { return SortFieldCase.updatedAt; } return SortFieldCase.createdAt; }; export const AllCases = React.memo(() => { - const [ - { data, isLoading, queryParams, filterOptions }, - setQueryParams, + const { + caseCount, + data, + dispatchUpdateCaseProperty, + filterOptions, + getCaseCount, + loading, + queryParams, + selectedCases, setFilters, - ] = useGetCases(); + setQueryParams, + setSelectedCases, + } = useGetCases(); const tableOnChangeCallback = useCallback( ({ page, sort }: EuiBasicTableOnChange) => { @@ -77,7 +117,13 @@ export const AllCases = React.memo(() => { [filterOptions, setFilters] ); - const memoizedGetCasesColumns = useMemo(() => getCasesColumns(), []); + const actions = useMemo( + () => + getActions({ caseStatus: filterOptions.state, dispatchUpdate: dispatchUpdateCaseProperty }), + [filterOptions.state, dispatchUpdateCaseProperty] + ); + + const memoizedGetCasesColumns = useMemo(() => getCasesColumns(actions), [filterOptions.state]); const memoizedPagination = useMemo( () => ({ pageIndex: queryParams.page - 1, @@ -88,55 +134,132 @@ export const AllCases = React.memo(() => { [data, queryParams] ); + const getBulkItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + + ), + [selectedCases, filterOptions.state] + ); + const sorting: EuiTableSortingType = { sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, }; + const euiBasicTableSelectionProps = useMemo>( + () => ({ + selectable: (item: Case) => true, + onSelectionChange: setSelectedCases, + }), + [selectedCases] + ); + const isCasesLoading = useMemo( + () => loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1, + [loading] + ); + const isDataEmpty = useMemo(() => data.total === 0, [data]); return ( - - + <> + + + + -1} + /> + + + -1} + /> + + + + {i18n.CREATE_TITLE} + + + + + + + + {isCasesLoading && !isDataEmpty && } + - - {isLoading && isEmpty(data.cases) && ( - - )} - {!isLoading && !isEmpty(data.cases) && ( - <> - - - - - {i18n.SHOWING_CASES(data.total ?? 0)} - - - - - {i18n.NO_CASES}} - titleSize="xs" - body={i18n.NO_CASES_BODY} - actions={ - - {i18n.ADD_NEW_CASE} - - } - /> - } - onChange={tableOnChangeCallback} - pagination={memoizedPagination} - sorting={sorting} - /> - - )} - + {isCasesLoading && isDataEmpty ? ( +
+ +
+ ) : ( +
+ + + + + {i18n.SHOWING_CASES(data.total ?? 0)} + + + + + {i18n.SELECTED_CASES(selectedCases.length)} + + + {i18n.BULK_ACTIONS} + + + + + {i18n.NO_CASES}} + titleSize="xs" + body={i18n.NO_CASES_BODY} + actions={ + + {i18n.ADD_NEW_CASE} + + } + /> + } + onChange={tableOnChangeCallback} + pagination={memoizedPagination} + selection={euiBasicTableSelectionProps} + sorting={sorting} + /> +
+ )} + + ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx index e593623788046..5256fb6d7b3ee 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx @@ -6,20 +6,22 @@ import React, { useCallback, useState } from 'react'; import { isEqual } from 'lodash/fp'; -import { EuiFieldSearch, EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiFieldSearch, + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import * as i18n from './translations'; import { FilterOptions } from '../../../../containers/case/types'; import { useGetTags } from '../../../../containers/case/use_get_tags'; -import { TagsFilterPopover } from '../../../../pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover'; +import { FilterPopover } from '../../../../components/filter_popover'; -interface Initial { - search: string; - tags: string[]; -} interface CasesTableFiltersProps { onFilterChanged: (filterOptions: Partial) => void; - initial: Initial; + initial: FilterOptions; } /** @@ -31,17 +33,18 @@ interface CasesTableFiltersProps { const CasesTableFiltersComponent = ({ onFilterChanged, - initial = { search: '', tags: [] }, + initial = { search: '', tags: [], state: 'open' }, }: CasesTableFiltersProps) => { const [search, setSearch] = useState(initial.search); const [selectedTags, setSelectedTags] = useState(initial.tags); - const [{ isLoading, data }] = useGetTags(); + const [showOpenCases, setShowOpenCases] = useState(initial.state === 'open'); + const [{ data }] = useGetTags(); const handleSelectedTags = useCallback( newTags => { if (!isEqual(newTags, selectedTags)) { setSelectedTags(newTags); - onFilterChanged({ search, tags: newTags }); + onFilterChanged({ tags: newTags }); } }, [search, selectedTags] @@ -51,12 +54,20 @@ const CasesTableFiltersComponent = ({ const trimSearch = newSearch.trim(); if (!isEqual(trimSearch, search)) { setSearch(trimSearch); - onFilterChanged({ tags: selectedTags, search: trimSearch }); + onFilterChanged({ search: trimSearch }); } }, [search, selectedTags] ); - + const handleToggleFilter = useCallback( + showOpen => { + if (showOpen !== showOpenCases) { + setShowOpenCases(showOpen); + onFilterChanged({ state: showOpen ? 'open' : 'closed' }); + } + }, + [showOpenCases] + ); return ( @@ -71,11 +82,32 @@ const CasesTableFiltersComponent = ({ - + {i18n.OPEN_CASES} + + + {i18n.CLOSED_CASES} + + {}} + selectedOptions={[]} + options={[]} + optionsEmptyLabel={i18n.NO_REPORTERS_AVAILABLE} + /> + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts index ab8e22ebcf1be..19117136ed046 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -8,9 +8,6 @@ import { i18n } from '@kbn/i18n'; export * from '../../translations'; -export const ALL_CASES = i18n.translate('xpack.siem.case.caseTable.title', { - defaultMessage: 'All Cases', -}); export const NO_CASES = i18n.translate('xpack.siem.case.caseTable.noCases.title', { defaultMessage: 'No Cases', }); @@ -21,6 +18,12 @@ export const ADD_NEW_CASE = i18n.translate('xpack.siem.case.caseTable.addNewCase defaultMessage: 'Add New Case', }); +export const SELECTED_CASES = (totalRules: number) => + i18n.translate('xpack.siem.case.caseTable.selectedCasesTitle', { + values: { totalRules }, + defaultMessage: 'Selected {totalRules} {totalRules, plural, =1 {case} other {cases}}', + }); + export const SHOWING_CASES = (totalRules: number) => i18n.translate('xpack.siem.case.caseTable.showingCasesTitle', { values: { totalRules }, @@ -33,16 +36,36 @@ export const UNIT = (totalCount: number) => defaultMessage: `{totalCount, plural, =1 {case} other {cases}}`, }); -export const SEARCH_CASES = i18n.translate( - 'xpack.siem.detectionEngine.case.caseTable.searchAriaLabel', - { - defaultMessage: 'Search cases', - } -); - -export const SEARCH_PLACEHOLDER = i18n.translate( - 'xpack.siem.detectionEngine.case.caseTable.searchPlaceholder', - { - defaultMessage: 'e.g. case name', - } -); +export const SEARCH_CASES = i18n.translate('xpack.siem.case.caseTable.searchAriaLabel', { + defaultMessage: 'Search cases', +}); + +export const BULK_ACTIONS = i18n.translate('xpack.siem.case.caseTable.bulkActions', { + defaultMessage: 'Bulk actions', +}); + +export const SEARCH_PLACEHOLDER = i18n.translate('xpack.siem.case.caseTable.searchPlaceholder', { + defaultMessage: 'e.g. case name', +}); +export const OPEN_CASES = i18n.translate('xpack.siem.case.caseTable.openCases', { + defaultMessage: 'Open cases', +}); +export const CLOSED_CASES = i18n.translate('xpack.siem.case.caseTable.closedCases', { + defaultMessage: 'Closed cases', +}); + +export const CLOSED = i18n.translate('xpack.siem.case.caseTable.closed', { + defaultMessage: 'Closed', +}); +export const DELETE = i18n.translate('xpack.siem.case.caseTable.delete', { + defaultMessage: 'Delete', +}); +export const REOPEN_CASE = i18n.translate('xpack.siem.case.caseTable.reopenCase', { + defaultMessage: 'Reopen case', +}); +export const CLOSE_CASE = i18n.translate('xpack.siem.case.caseTable.closeCase', { + defaultMessage: 'Close case', +}); +export const DUPLICATE_CASE = i18n.translate('xpack.siem.case.caseTable.duplicateCase', { + defaultMessage: 'Duplicate case', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx new file mode 100644 index 0000000000000..2fe25a7d1f5d0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiContextMenuItem } from '@elastic/eui'; +import React from 'react'; +import * as i18n from './translations'; +import { Case } from '../../../../containers/case/types'; + +interface GetBulkItems { + // cases: Case[]; + closePopover: () => void; + // dispatch: Dispatch; + // dispatchToaster: Dispatch; + // reFetchCases: (refreshPrePackagedCase?: boolean) => void; + selectedCases: Case[]; + caseStatus: string; +} + +export const getBulkItems = ({ + // cases, + closePopover, + caseStatus, + // dispatch, + // dispatchToaster, + // reFetchCases, + selectedCases, +}: GetBulkItems) => { + return [ + caseStatus === 'open' ? ( + { + closePopover(); + // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); + // reFetchCases(true); + }} + > + {i18n.BULK_ACTION_CLOSE_SELECTED} + + ) : ( + { + closePopover(); + // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); + // reFetchCases(true); + }} + > + {i18n.BULK_ACTION_OPEN_SELECTED} + + ), + { + closePopover(); + // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); + // reFetchCases(true); + }} + > + {i18n.BULK_ACTION_DELETE_SELECTED} + , + ]; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts new file mode 100644 index 0000000000000..0bf213868bd76 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const BULK_ACTION_CLOSE_SELECTED = i18n.translate( + 'xpack.siem.case.caseTable.bulkActions.closeSelectedTitle', + { + defaultMessage: 'Close selected', + } +); + +export const BULK_ACTION_OPEN_SELECTED = i18n.translate( + 'xpack.siem.case.caseTable.bulkActions.openSelectedTitle', + { + defaultMessage: 'Open selected', + } +); + +export const BULK_ACTION_DELETE_SELECTED = i18n.translate( + 'xpack.siem.case.caseTable.bulkActions.deleteSelectedTitle', + { + defaultMessage: 'Delete selected', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx new file mode 100644 index 0000000000000..8d0fafdfc36ca --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Dispatch, useEffect, useMemo } from 'react'; +import { EuiDescriptionList, EuiLoadingSpinner } from '@elastic/eui'; +import * as i18n from '../all_cases/translations'; +import { CaseCount } from '../../../../containers/case/use_get_cases'; + +export interface Props { + caseCount: CaseCount; + caseState: 'open' | 'closed'; + getCaseCount: Dispatch; + isLoading: boolean; +} + +export const OpenClosedStats = React.memo( + ({ caseCount, caseState, getCaseCount, isLoading }) => { + useEffect(() => { + getCaseCount(caseState); + }, [caseState]); + + const openClosedStats = useMemo( + () => [ + { + title: caseState === 'open' ? i18n.OPEN_CASES : i18n.CLOSED_CASES, + description: isLoading ? : caseCount[caseState], + }, + ], + [caseCount, caseState, isLoading] + ); + return ; + } +); + +OpenClosedStats.displayName = 'OpenClosedStats'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 5f0509586fc81..fc64bd64ec4a2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -18,8 +18,8 @@ export const NAME = i18n.translate('xpack.siem.case.caseView.name', { defaultMessage: 'Name', }); -export const CREATED_AT = i18n.translate('xpack.siem.case.caseView.createdAt', { - defaultMessage: 'Created at', +export const OPENED_ON = i18n.translate('xpack.siem.case.caseView.openedOn', { + defaultMessage: 'Opened on', }); export const REPORTER = i18n.translate('xpack.siem.case.caseView.createdBy', { @@ -88,6 +88,21 @@ export const TAGS = i18n.translate('xpack.siem.case.caseView.tags', { defaultMessage: 'Tags', }); +export const NO_TAGS_AVAILABLE = i18n.translate('xpack.siem.case.allCases.noTagsAvailable', { + defaultMessage: 'No tags available', +}); + +export const NO_REPORTERS_AVAILABLE = i18n.translate( + 'xpack.siem.case.caseView.noReportersAvailable', + { + defaultMessage: 'No reporters available.', + } +); + +export const COMMENTS = i18n.translate('xpack.siem.case.allCases.comments', { + defaultMessage: 'Comments', +}); + export const TAGS_HELP = i18n.translate('xpack.siem.case.createCase.fieldTagsHelpText', { defaultMessage: 'Type one or more custom identifying tags for this case. Press enter after each tag to begin a new one.', diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx index 4c7cfac33c546..31420ad07cd50 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx @@ -13,7 +13,7 @@ import { UtilityBarGroup, UtilityBarSection, UtilityBarText, -} from '../../../../components/detection_engine/utility_bar'; +} from '../../../../components/utility_bar'; import { columns } from './columns'; import { ColumnTypes, PageTypes, SortTypes } from './types'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx index 86772eb0e155d..25c0424cadf11 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx @@ -13,7 +13,7 @@ import { UtilityBarGroup, UtilityBarSection, UtilityBarText, -} from '../../../../../components/detection_engine/utility_bar'; +} from '../../../../../components/utility_bar'; import * as i18n from './translations'; import { useUiSetting$ } from '../../../../../lib/kibana'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../../common/constants'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx index 9676b83a26f55..e7d68164c4ef4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx @@ -30,7 +30,7 @@ import { UtilityBarGroup, UtilityBarSection, UtilityBarText, -} from '../../../../components/detection_engine/utility_bar'; +} from '../../../../components/utility_bar'; import { useStateToaster } from '../../../../components/toasters'; import { Loader } from '../../../../components/loader'; import { Panel } from '../../../../components/panel'; From e0022be6d3dcb41788519853c38d754710df9c96 Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Thu, 5 Mar 2020 13:08:29 -0600 Subject: [PATCH 02/12] wait for any text in dialog (#59352) Co-authored-by: Elastic Machine --- test/functional/services/saved_query_management_component.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index b94558c209e6a..244c1cd214de5 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -164,6 +164,10 @@ export function SavedQueryManagementComponentProvider({ getService }: FtrProvide if (isOpenAlready) return; await testSubjects.click('saved-query-management-popover-button'); + await retry.waitFor('saved query management popover to have any text', async () => { + const queryText = await testSubjects.getVisibleText('saved-query-management-popover'); + return queryText.length > 0; + }); } async closeSavedQueryManagementComponent() { From 096dda6f3460eee926661c7271a397d874e739a5 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Thu, 5 Mar 2020 12:27:52 -0700 Subject: [PATCH 03/12] Upgrade EUI to v20.0.2 (#59199) * Updated EUI to 20.0.1; updated typescript usage * snapshots * Upgrade to eui 20.0.2, fix one more type * PR feedback * Update EUI icon usage to the correct types * Updated with master --- package.json | 2 +- packages/kbn-ui-shared-deps/package.json | 2 +- .../public/components/editor/field_select.tsx | 8 +- .../public/components/vis/list_control.tsx | 2 +- .../public/input_control_vis_type.ts | 2 +- .../public/components/agg_select.tsx | 4 +- .../public/components/controls/field.test.tsx | 2 +- .../public/components/controls/field.tsx | 4 +- .../components/controls/time_interval.tsx | 6 +- .../public/components/timelion_interval.tsx | 4 +- .../__snapshots__/icon_select.test.js.snap | 1 + .../splits/__snapshots__/terms.test.js.snap | 1 + .../vis_type_vislib/public/heatmap.ts | 2 +- .../field/__snapshots__/field.test.tsx.snap | 12 - .../components/field/field.test.tsx | 6 +- .../management_app/components/field/field.tsx | 15 +- .../filter_editor/generic_combo_box.tsx | 6 +- .../index_pattern_select.tsx | 2 +- .../components/fields/combobox_field.tsx | 4 +- .../static/forms/helpers/de_serializers.ts | 6 +- .../min_selectable_selection.ts | 4 +- .../static/forms/helpers/serializers.ts | 4 +- .../inspector_panel.test.tsx.snap | 32 +- .../plugins/kbn_tp_run_pipeline/package.json | 2 +- .../kbn_tp_custom_visualizations/package.json | 2 +- .../self_changing_vis/self_changing_vis.js | 2 +- .../kbn_tp_embeddable_explorer/package.json | 2 +- .../kbn_tp_sample_panel_action/package.json | 2 +- typings/@elastic/eui/index.d.ts | 1 - .../public/components/inputs/code_editor.tsx | 2 - .../asset_manager/asset_manager.tsx | 3 +- .../components/asset_manager/asset_modal.tsx | 2 +- .../custom_element_modal.tsx | 5 +- .../keyboard_shortcuts_doc.stories.storyshot | 2096 +++++++++-------- .../components/field_manager/field_editor.tsx | 13 +- .../dimension_panel/field_select.tsx | 8 +- .../create_analytics_form.tsx | 12 +- .../hooks/use_create_analytics_form/state.ts | 6 +- .../__snapshots__/overrides.test.js.snap | 3 + .../__snapshots__/editor.test.tsx.snap | 4 + .../components/custom_url_editor/editor.tsx | 10 +- .../ml_job_editor/ml_job_editor.tsx | 4 +- .../common/components/job_groups_input.tsx | 12 +- .../time_field/time_field_select.tsx | 11 +- .../calendars/calendars_selection.tsx | 6 +- .../components/groups/groups_input.tsx | 12 +- .../advanced_detector_modal.tsx | 30 +- .../components/agg_select/agg_select.tsx | 10 +- .../categorization_field_select.tsx | 8 +- .../influencers/influencers_select.tsx | 8 +- .../split_field/split_field_select.tsx | 8 +- .../summary_count_field_select.tsx | 8 +- .../entity_control/entity_control.tsx | 12 +- .../report_info_button.test.tsx.snap | 96 +- .../components/edit_data_provider/helpers.tsx | 14 +- .../components/edit_data_provider/index.tsx | 12 +- .../timeline/search_super_select/index.tsx | 8 +- .../detection_engine/rules/all/columns.tsx | 2 +- .../components/import_rule_modal/index.tsx | 4 +- .../detection_engine/rules/details/index.tsx | 2 +- .../aggregation_dropdown/dropdown.tsx | 6 +- .../components/step_define/common.ts | 6 +- .../ping_list/__tests__/ping_list.test.tsx | 4 +- x-pack/package.json | 2 +- .../add_log_column_popover.tsx | 8 +- .../top_categories/datasets_selector.tsx | 4 +- .../remote_cluster_form.test.js.snap | 4 +- .../role_combo_box/role_combo_box_option.tsx | 4 +- .../json_rule_editor.test.tsx | 22 +- .../cluster_privileges.test.tsx.snap | 1 + .../elasticsearch_privileges.test.tsx.snap | 1 + .../index_privilege_form.test.tsx.snap | 2 + .../privileges/es/index_privilege_form.tsx | 10 +- .../space_selector.tsx | 8 +- .../policy_form/steps/step_settings.tsx | 10 +- .../type_settings/hdfs_settings.tsx | 17 +- .../steps/step_logistics.tsx | 10 +- .../steps/step_review.tsx | 10 +- .../steps/step_settings.tsx | 10 +- .../policy_details/tabs/tab_history.tsx | 17 +- .../type_details/default_details.tsx | 17 +- .../customize_space_avatar.test.tsx.snap | 9 +- .../customize_space_avatar.tsx | 7 +- .../builtin_action_types/webhook.tsx | 2 - .../threshold/expression.tsx | 6 +- .../common/expression_items/of.test.tsx | 56 +- .../json_watch_edit_simulate.tsx | 1 - .../action_fields/webhook_action_fields.tsx | 1 - .../threshold_watch_edit.tsx | 4 +- x-pack/typings/@elastic/eui/index.d.ts | 1 - yarn.lock | 16 +- 91 files changed, 1436 insertions(+), 1413 deletions(-) diff --git a/package.json b/package.json index 2c401724c72cd..9f12f04223103 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "@elastic/charts": "^17.1.1", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "7.6.0", - "@elastic/eui": "19.0.0", + "@elastic/eui": "20.0.2", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.4.0", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index e9ad227b235fa..65fd837ad17c2 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -11,7 +11,7 @@ "devDependencies": { "@elastic/charts": "^17.1.1", "abortcontroller-polyfill": "^1.4.0", - "@elastic/eui": "19.0.0", + "@elastic/eui": "20.0.2", "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@kbn/i18n": "1.0.0", diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.tsx index bde2f09ab0a47..68cca9bf6c4f2 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.tsx @@ -22,13 +22,13 @@ import React, { Component } from 'react'; import { InjectedIntlProps } from 'react-intl'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; -import { EuiFormRow, EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { IIndexPattern, IFieldType } from '../../../../../../plugins/data/public'; interface FieldSelectUiState { isLoading: boolean; - fields: Array>; + fields: Array>; indexPatternId: string; } @@ -105,7 +105,7 @@ class FieldSelectUi extends Component { } const fieldsByTypeMap = new Map(); - const fields: Array> = []; + const fields: Array> = []; indexPattern.fields .filter(this.props.filterField ?? (() => true)) .forEach((field: IFieldType) => { @@ -135,7 +135,7 @@ class FieldSelectUi extends Component { }); }, 300); - onChange = (selectedOptions: Array>) => { + onChange = (selectedOptions: Array>) => { this.props.onChange(_.get(selectedOptions, '0.value')); }; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx b/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx index d01cef15ea41b..6ded66917a3fd 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx @@ -76,7 +76,7 @@ class ListControlUi extends PureComponent { + setTextInputRef = (ref: HTMLInputElement | null) => { this.textInput = ref; }; diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts index 9473ea5a20b35..1bdff06b3a59f 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts +++ b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts @@ -34,7 +34,7 @@ export function createInputControlVisTypeDefinition(deps: InputControlVisDepende title: i18n.translate('inputControl.register.controlsTitle', { defaultMessage: 'Controls', }), - icon: 'visControls', + icon: 'controlsHorizontal', description: i18n.translate('inputControl.register.controlsDescription', { defaultMessage: 'Create interactive controls for easy dashboard manipulation.', }), diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx index 9a408c2d98b22..4d969a2d8ec6c 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx @@ -19,7 +19,7 @@ import { get, has } from 'lodash'; import React, { useEffect, useCallback, useState } from 'react'; -import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow, EuiLink, EuiText } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, EuiLink, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -104,7 +104,7 @@ function DefaultEditorAggSelect({ const isValid = !!value && !errors.length && !isDirty; const onChange = useCallback( - (options: EuiComboBoxOptionProps[]) => { + (options: EuiComboBoxOptionOption[]) => { const selectedOption = get(options, '0.target'); if (selectedOption) { setValue(selectedOption as IAggType); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.test.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.test.tsx index 36496c2800b64..186738d0f551c 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.test.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.test.tsx @@ -29,7 +29,7 @@ import { FieldParamEditor, FieldParamEditorProps } from './field'; import { IAggConfig } from '../../legacy_imports'; function callComboBoxOnChange(comp: ReactWrapper, value: any = []) { - const comboBoxProps: EuiComboBoxProps = comp.find(EuiComboBox).props(); + const comboBoxProps = comp.find(EuiComboBox).props() as EuiComboBoxProps; if (comboBoxProps.onChange) { comboBoxProps.onChange(value); } diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx index d605fb203f4d3..0ec00ab6f20f0 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx @@ -20,7 +20,7 @@ import { get } from 'lodash'; import React, { useEffect, useState, useCallback } from 'react'; -import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { IndexPatternField } from 'src/plugins/data/public'; @@ -55,7 +55,7 @@ function FieldParamEditor({ ? [{ label: value.displayName || value.name, target: value }] : []; - const onChange = (options: EuiComboBoxOptionProps[]) => { + const onChange = (options: EuiComboBoxOptionOption[]) => { const selectedOption: IndexPatternField = get(options, '0.target'); if (!(aggParam.required && !selectedOption)) { setValue(selectedOption); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx index 5da0d6462a8ba..ee3666b2ed441 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx @@ -19,14 +19,14 @@ import { get, find } from 'lodash'; import React, { useEffect } from 'react'; -import { EuiFormRow, EuiIconTip, EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiFormRow, EuiIconTip, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { isValidInterval, AggParamOption } from '../../legacy_imports'; import { AggParamEditorProps } from '../agg_param_props'; -interface ComboBoxOption extends EuiComboBoxOptionProps { +interface ComboBoxOption extends EuiComboBoxOptionOption { key: string; } @@ -105,7 +105,7 @@ function TimeIntervalParamEditor({ } }; - const onChange = (opts: EuiComboBoxOptionProps[]) => { + const onChange = (opts: EuiComboBoxOptionOption[]) => { const selectedOpt: ComboBoxOption = get(opts, '0'); setValue(selectedOpt ? selectedOpt.key : ''); diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx index 02783434bfdc2..13a57296bab7a 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx +++ b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_interval.tsx @@ -18,7 +18,7 @@ */ import React, { useMemo, useCallback } from 'react'; -import { EuiFormRow, EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isValidEsInterval } from '../../../../core_plugins/data/common'; @@ -90,7 +90,7 @@ function TimelionInterval({ value, setValue, setValidity }: TimelionIntervalProp ); const onChange = useCallback( - (opts: Array>) => { + (opts: Array>) => { setValue((opts[0] && opts[0].value) || ''); }, [setValue] diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/__snapshots__/icon_select.test.js.snap b/src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/__snapshots__/icon_select.test.js.snap index fd22bcafb8df4..d269f61beefab 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/__snapshots__/icon_select.test.js.snap +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/icon_select/__snapshots__/icon_select.test.js.snap @@ -2,6 +2,7 @@ exports[`src/legacy/core_plugins/metrics/public/components/icon_select/icon_select.js should render and match a snapshot 1`] = ` ({ name: 'heatmap', title: i18n.translate('visTypeVislib.heatmap.heatmapTitle', { defaultMessage: 'Heat Map' }), - icon: 'visHeatmap', + icon: 'heatmap', description: i18n.translate('visTypeVislib.heatmap.heatmapDescription', { defaultMessage: 'Shade cells within a matrix', }), diff --git a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap index dba1678339f24..88f30e03df052 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap +++ b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap @@ -1350,7 +1350,6 @@ exports[`Field for json setting should render as read only if saving is disabled "$blockScrolling": Infinity, } } - fullWidth={true} height="auto" isReadOnly={true} maxLines={30} @@ -1456,7 +1455,6 @@ exports[`Field for json setting should render as read only with help text if ove "$blockScrolling": Infinity, } } - fullWidth={true} height="auto" isReadOnly={true} maxLines={30} @@ -1538,7 +1536,6 @@ exports[`Field for json setting should render custom setting icon if it is custo "$blockScrolling": Infinity, } } - fullWidth={true} height="auto" isReadOnly={false} maxLines={30} @@ -1651,7 +1648,6 @@ exports[`Field for json setting should render default value if there is no user "$blockScrolling": Infinity, } } - fullWidth={true} height="auto" isReadOnly={false} maxLines={30} @@ -1740,7 +1736,6 @@ exports[`Field for json setting should render unsaved value if there are unsaved "$blockScrolling": Infinity, } } - fullWidth={true} height="auto" isReadOnly={false} maxLines={30} @@ -1864,7 +1859,6 @@ exports[`Field for json setting should render user value if there is user value "$blockScrolling": Infinity, } } - fullWidth={true} height="auto" isReadOnly={false} maxLines={30} @@ -1935,7 +1929,6 @@ exports[`Field for markdown setting should render as read only if saving is disa "$blockScrolling": Infinity, } } - fullWidth={true} height="auto" isReadOnly={true} maxLines={30} @@ -2038,7 +2031,6 @@ exports[`Field for markdown setting should render as read only with help text if "$blockScrolling": Infinity, } } - fullWidth={true} height="auto" isReadOnly={true} maxLines={30} @@ -2120,7 +2112,6 @@ exports[`Field for markdown setting should render custom setting icon if it is c "$blockScrolling": Infinity, } } - fullWidth={true} height="auto" isReadOnly={false} maxLines={30} @@ -2191,7 +2182,6 @@ exports[`Field for markdown setting should render default value if there is no u "$blockScrolling": Infinity, } } - fullWidth={true} height="auto" isReadOnly={false} maxLines={30} @@ -2280,7 +2270,6 @@ exports[`Field for markdown setting should render unsaved value if there are uns "$blockScrolling": Infinity, } } - fullWidth={true} height="auto" isReadOnly={false} maxLines={30} @@ -2397,7 +2386,6 @@ exports[`Field for markdown setting should render user value if there is user va "$blockScrolling": Infinity, } } - fullWidth={true} height="auto" isReadOnly={false} maxLines={30} diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx index 8e41fed685898..356e38c799659 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx @@ -363,7 +363,7 @@ describe('Field', () => { (component.instance() as Field).getImageAsBase64 = ({}: Blob) => Promise.resolve(''); it('should be able to change value and cancel', async () => { - (component.instance() as Field).onImageChange([userValue]); + (component.instance() as Field).onImageChange(([userValue] as unknown) as FileList); expect(handleChange).toBeCalled(); await wrapper.setProps({ unsavedChanges: { @@ -387,7 +387,9 @@ describe('Field', () => { const updated = wrapper.update(); findTestSubject(updated, `advancedSetting-changeImage-${setting.name}`).simulate('click'); const newUserValue = `${userValue}=`; - await (component.instance() as Field).onImageChange([newUserValue]); + await (component.instance() as Field).onImageChange(([ + newUserValue, + ] as unknown) as FileList); expect(handleChange).toBeCalled(); }); diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx index 18a1a365709d1..60d2b55dfceb4 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -90,7 +90,7 @@ export const getEditableValue = ( }; export class Field extends PureComponent { - private changeImageForm: EuiFilePicker | undefined = React.createRef(); + private changeImageForm = React.createRef(); getDisplayedDefaultValue( type: UiSettingsType, @@ -138,7 +138,7 @@ export class Field extends PureComponent { } } - onCodeEditorChange = (value: UiSettingsType) => { + onCodeEditorChange = (value: string) => { const { defVal, type } = this.props.setting; let newUnsavedValue; @@ -212,7 +212,9 @@ export class Field extends PureComponent { }); }; - onImageChange = async (files: any[]) => { + onImageChange = async (files: FileList | null) => { + if (files == null) return; + if (!files.length) { this.setState({ unsavedValue: null, @@ -278,9 +280,9 @@ export class Field extends PureComponent { }; cancelChangeImage = () => { - if (this.changeImageForm.current) { - this.changeImageForm.current.fileInput.value = null; - this.changeImageForm.current.handleChange({}); + if (this.changeImageForm.current?.fileInput) { + this.changeImageForm.current.fileInput.value = ''; + this.changeImageForm.current.handleChange(); } if (this.props.clearChange) { this.props.clearChange(this.props.setting.name); @@ -352,7 +354,6 @@ export class Field extends PureComponent { $blockScrolling: Infinity, }} showGutter={false} - fullWidth /> ); diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx index 9d541af5a1d17..a5db8b66caa01 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/generic_combo_box.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import React from 'react'; export interface GenericComboBoxProps { @@ -38,7 +38,7 @@ export function GenericComboBox(props: GenericComboBoxProps) { const { options, selectedOptions, getLabel, onChange, ...otherProps } = props; const labels = options.map(getLabel); - const euiOptions: EuiComboBoxOptionProps[] = labels.map(label => ({ label })); + const euiOptions: EuiComboBoxOptionOption[] = labels.map(label => ({ label })); const selectedEuiOptions = selectedOptions .filter(option => { return options.indexOf(option) !== -1; @@ -47,7 +47,7 @@ export function GenericComboBox(props: GenericComboBoxProps) { return euiOptions[options.indexOf(option)]; }); - const onComboBoxChange = (newOptions: EuiComboBoxOptionProps[]) => { + const onComboBoxChange = (newOptions: EuiComboBoxOptionOption[]) => { const newValues = newOptions.map(({ label }) => { return options[labels.indexOf(label)]; }); diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index 829c8205a8b52..c56060bb9c288 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -39,7 +39,7 @@ export type IndexPatternSelectProps = Required< interface IndexPatternSelectState { isLoading: boolean; options: []; - selectedIndexPattern: string | undefined; + selectedIndexPattern: { value: string; label: string } | undefined; searchValue: string | undefined; } diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx index 3613867950098..a10da62fa6906 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { FieldHook, VALIDATION_TYPES, FieldValidateResponse } from '../../hook_form_lib'; @@ -69,7 +69,7 @@ export const ComboBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) => field.setValue(newValue); }; - const onComboChange = (options: EuiComboBoxOptionProps[]) => { + const onComboChange = (options: EuiComboBoxOptionOption[]) => { field.setValue(options.map(option => option.label)); }; diff --git a/src/plugins/es_ui_shared/static/forms/helpers/de_serializers.ts b/src/plugins/es_ui_shared/static/forms/helpers/de_serializers.ts index f4b528e681d43..274aa82b31834 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/de_serializers.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/de_serializers.ts @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { Option } from '@elastic/eui/src/components/selectable/types'; +import { EuiSelectableOption } from '@elastic/eui'; import { SerializerFunc } from '../hook_form_lib'; -type FuncType = (selectOptions: Option[]) => SerializerFunc; +type FuncType = (selectOptions: EuiSelectableOption[]) => SerializerFunc; export const multiSelectComponent: Record = { // This deSerializer takes the previously selected options and map them @@ -31,7 +31,7 @@ export const multiSelectComponent: Record = { return selectOptions; } - return (selectOptions as Option[]).map(option => ({ + return (selectOptions as EuiSelectableOption[]).map(option => ({ ...option, checked: (defaultFormValue as string[]).includes(option.label) ? 'on' : undefined, })); diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/min_selectable_selection.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/min_selectable_selection.ts index a10371d08ad5a..8f75c45df6c4a 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/min_selectable_selection.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/min_selectable_selection.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Option } from '@elastic/eui/src/components/selectable/types'; +import { EuiSelectableOption } from '@elastic/eui'; import { ValidationFunc, ValidationError } from '../../hook_form_lib'; import { hasMinLengthArray } from '../../../validators/array'; @@ -42,7 +42,7 @@ export const minSelectableSelectionField = ({ // We need to convert all the options from the multi selectable component, to the // an actual Array of selection _before_ validating the Array length. - return hasMinLengthArray(total)(optionsToSelectedValue(value as Option[])) + return hasMinLengthArray(total)(optionsToSelectedValue(value as EuiSelectableOption[])) ? undefined : { code: 'ERR_MIN_SELECTION', diff --git a/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts b/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts index 0bb89cc1af593..bae6b4c2652ca 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts @@ -36,7 +36,7 @@ * ```` */ -import { Option } from '@elastic/eui/src/components/selectable/types'; +import { EuiSelectableOption } from '@elastic/eui'; import { SerializerFunc } from '../hook_form_lib'; export const multiSelectComponent: Record> = { @@ -45,7 +45,7 @@ export const multiSelectComponent: Record> = { * * @param value The Eui Selectable options array */ - optionsToSelectedValue(options: Option[]): string[] { + optionsToSelectedValue(options: EuiSelectableOption[]): string[] { return options.filter(option => option.checked === 'on').map(option => option.label); }, }; diff --git a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap index 9cf725a2faa73..fcd03df5637d0 100644 --- a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap +++ b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap @@ -326,21 +326,25 @@ exports[`InspectorPanel should render as expected 1`] = `
- -

- View 1 -

-
+ +

+ View 1 +

+
+
diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index cb0b9de01c4ed..594823ad047a7 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "19.0.0", + "@elastic/eui": "20.0.2", "react": "^16.12.0", "react-dom": "^16.12.0" } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index c68ef6dcd0202..56f5719b5dbef 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "19.0.0", + "@elastic/eui": "20.0.2", "react": "^16.12.0" } } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js index 1c6acab4aba16..2976a6cd98e30 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js @@ -25,7 +25,7 @@ import { setup as visualizations } from '../../../../../../src/legacy/core_plugi visualizations.types.createReactVisualization({ name: 'self_changing_vis', title: 'Self Changing Vis', - icon: 'visControls', + icon: 'controlsHorizontal', description: 'This visualization is able to change its own settings, that you could also set in the editor.', visConfig: { diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index d4e4c6bf2fee0..d12c15d0688b2 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "19.0.0", + "@elastic/eui": "20.0.2", "react": "^16.12.0" }, "scripts": { diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index 3ade079419a55..eb24035f9acbe 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "19.0.0", + "@elastic/eui": "20.0.2", "react": "^16.12.0" }, "scripts": { diff --git a/typings/@elastic/eui/index.d.ts b/typings/@elastic/eui/index.d.ts index 9268f72724141..db07861d63cfe 100644 --- a/typings/@elastic/eui/index.d.ts +++ b/typings/@elastic/eui/index.d.ts @@ -21,6 +21,5 @@ import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; // TODO: Remove once typescript definitions are in EUI declare module '@elastic/eui' { - export const EuiCodeEditor: React.FC; export const Query: any; } diff --git a/x-pack/legacy/plugins/beats_management/public/components/inputs/code_editor.tsx b/x-pack/legacy/plugins/beats_management/public/components/inputs/code_editor.tsx index 6ec2a7f02f3a3..46ea90a9c1b30 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/inputs/code_editor.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/inputs/code_editor.tsx @@ -93,13 +93,11 @@ class CodeEditor extends Component< error={error ? getErrorMessage() : []} > { this.props.onAssetDelete(this.state.deleteId); }; - private handleFileUpload = (files: FileList) => { + private handleFileUpload = (files: FileList | null) => { + if (files == null) return; this.setState({ isLoading: true }); Promise.all(Array.from(files).map(file => this.props.onAssetAdd(file))).finally(() => { this.setState({ isLoading: false }); diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx index f8bce19a46968..3dfbb1b1fde3c 100644 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx @@ -43,7 +43,7 @@ interface Props { /** Function to invoke when the modal is closed */ onClose: () => void; /** Function to invoke when a file is uploaded */ - onFileUpload: (assets: FileList) => void; + onFileUpload: (assets: FileList | null) => void; /** Function to invoke when an asset is copied */ onAssetCopy: (asset: AssetType) => void; /** Function to invoke when an asset is created */ diff --git a/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx b/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx index bd7fc775a34a0..56bd0bf5e9f2a 100644 --- a/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx @@ -100,8 +100,9 @@ export class CustomElementModal extends PureComponent { this.setState({ [type]: value }); }; - private _handleUpload = (files: File[]) => { - const [file] = files; + private _handleUpload = (files: FileList | null) => { + if (files == null) return; + const file = files[0]; const [type, subtype] = get(file, 'type', '').split('/'); if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) { encode(file).then((dataurl: string) => this._handleChange('image', dataurl)); diff --git a/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot index 35cdd5ac378f4..9954ae0147a97 100644 --- a/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot @@ -82,1060 +82,1064 @@ exports[`Storyshots components/KeyboardShortcutsDoc default 1`] = ` className="euiFlyoutBody__overflow" >
-

- Element controls -

-
-
-
- Cut -
-
- - - - CTRL - - - - - - X - - - -
-
- Copy -
-
- - - - CTRL - - - - - - C - - - -
-
- Paste -
-
- - - - CTRL - - - - - - V - - - -
-
- Clone -
-
- - - - CTRL - - - - - - D - - - -
-
- Delete -
-
- - - - DEL - - - - - - or - - - - - - BACKSPACE - - - -
-
- Bring forward -
-
- - - - CTRL - - - - - - ↑ - - - -
-
- Bring to front -
-
- - - - CTRL - - - - - - SHIFT - - - - - - ↑ - - - -
-
- Send backward -
-
- - - - CTRL - - - - - - ↓ - - - -
-
- Send to back -
-
- - - - CTRL - - - - - - SHIFT - - - - - - ↓ - - - -
-
- Group -
-
- - - - G - - - -
-
- Ungroup -
-
- - - - U - - - -
-
- Shift up by 10px -
-
- - - - ↑ - - - -
-
- Shift down by 10px -
-
- - - - ↓ - - - -
-
- Shift left by 10px -
-
- - - - ← - - - -
-
- Shift right by 10px -
-
- - - - → - - - -
-
- Shift up by 1px -
-
- - - - SHIFT - - - - - - ↑ - - - -
-
- Shift down by 1px -
-
- - - - SHIFT - - - - - - ↓ - - - -
-
- Shift left by 1px -
-
- - - - SHIFT - - - - - - ← - - - -
-
- Shift right by 1px -
-
- - - - SHIFT - - - - - - → - - - -
-
-
-
-

- Expression controls -

-
-
-
- Run whole expression -
-
- - - - CTRL - - - - - - ENTER - - - -
-
+

+ Element controls +

+
+
+
+ Cut +
+
+ + + + CTRL + + + + + + X + + + +
+
+ Copy +
+
+ + + + CTRL + + + + + + C + + + +
+
+ Paste +
+
+ + + + CTRL + + + + + + V + + + +
+
+ Clone +
+
+ + + + CTRL + + + + + + D + + + +
+
+ Delete +
+
+ + + + DEL + + + + + + or + + + + + + BACKSPACE + + + +
+
+ Bring forward +
+
+ + + + CTRL + + + + + + ↑ + + + +
+
+ Bring to front +
+
+ + + + CTRL + + + + + + SHIFT + + + + + + ↑ + + + +
+
+ Send backward +
+
+ + + + CTRL + + + + + + ↓ + + + +
+
+ Send to back +
+
+ + + + CTRL + + + + + + SHIFT + + + + + + ↓ + + + +
+
+ Group +
+
+ + + + G + + + +
+
+ Ungroup +
+
+ + + + U + + + +
+
+ Shift up by 10px +
+
+ + + + ↑ + + + +
+
+ Shift down by 10px +
+
+ + + + ↓ + + + +
+
+ Shift left by 10px +
+
+ + + + ← + + + +
+
+ Shift right by 10px +
+
+ + + + → + + + +
+
+ Shift up by 1px +
+
+ + + + SHIFT + + + + + + ↑ + + + +
+
+ Shift down by 1px +
+
+ + + + SHIFT + + + + + + ↓ + + + +
+
+ Shift left by 1px +
+
+ + + + SHIFT + + + + + + ← + + + +
+
+ Shift right by 1px +
+
+ + + + SHIFT + + + + + + → + + + +
+
+
+
-
-
-

- Editor controls -

-
-
-
- Select multiple elements -
-
- - - - SHIFT - - - - - - CLICK - - - -
-
- Resize from center -
-
- - - - ALT - - - - - - DRAG - - - -
-
- Move, resize, and rotate without snapping -
-
- - - - CTRL - - - - - - DRAG - - - -
-
- Select element below -
-
- - - - CTRL - - - - - - CLICK - - - -
-
- Undo last action -
-
- - - - CTRL - - - - - - Z - - - -
-
- Redo last action -
-
- - - - CTRL - - - - - - SHIFT - - - - - - Z - - - -
-
- Go to previous page -
-
- - - - ALT - - - - - - [ - - - -
-
- Go to next page -
-
- - - - ALT - - - - - - ] - - - -
-
- Toggle edit mode -
-
- - - - ALT - - - - - - E - - - -
-
- Show grid -
-
- - - - ALT - - - - - - G - - - -
-
- Refresh workpad -
-
- - - - ALT - - - - - - R - - - -
-
- Zoom in -
-
- - - - CTRL - - - - - - ALT - - - - - - + - - - -
-
- Zoom out -
-
- - - - CTRL - - - - - - ALT - - - - - - - - - - -
-
- Reset zoom to 100% -
-
- - - - CTRL - - - - - - ALT - - - - - - [ - - - -
-
- Enter presentation mode -
-
- - - - ALT - - - - - - F - - - - - - or - - - - - - ALT - - - - - - P - - - -
-
+

+ Expression controls +

+
+
+
+ Run whole expression +
+
+ + + + CTRL + + + + + + ENTER + + + +
+
+
+
-
-
-

- Presentation controls -

-
-
-
- Enter presentation mode -
-
- - - - ALT - - - - - - F - - - - - - or - - - - - - ALT - - - - - - P - - - -
-
- Exit presentation mode -
-
- - - - ESC - - - -
-
- Go to previous page -
-
- - - - ALT - - - - - - [ - - - - - - or - - - - - - BACKSPACE - - - - - - or - - - - - - ← - - - -
-
- Go to next page -
-
- - - - ALT - - - - - - ] - - - - - - or - - - - - - SPACE - - - - - - or - - - - - - → - - - -
-
- Refresh workpad -
-
- - - - ALT - - - - - - R - - - -
-
- Toggle page cycling -
-
- - - - P - - - -
-
+

+ Editor controls +

+
+
+
+ Select multiple elements +
+
+ + + + SHIFT + + + + + + CLICK + + + +
+
+ Resize from center +
+
+ + + + ALT + + + + + + DRAG + + + +
+
+ Move, resize, and rotate without snapping +
+
+ + + + CTRL + + + + + + DRAG + + + +
+
+ Select element below +
+
+ + + + CTRL + + + + + + CLICK + + + +
+
+ Undo last action +
+
+ + + + CTRL + + + + + + Z + + + +
+
+ Redo last action +
+
+ + + + CTRL + + + + + + SHIFT + + + + + + Z + + + +
+
+ Go to previous page +
+
+ + + + ALT + + + + + + [ + + + +
+
+ Go to next page +
+
+ + + + ALT + + + + + + ] + + + +
+
+ Toggle edit mode +
+
+ + + + ALT + + + + + + E + + + +
+
+ Show grid +
+
+ + + + ALT + + + + + + G + + + +
+
+ Refresh workpad +
+
+ + + + ALT + + + + + + R + + + +
+
+ Zoom in +
+
+ + + + CTRL + + + + + + ALT + + + + + + + + + + +
+
+ Zoom out +
+
+ + + + CTRL + + + + + + ALT + + + + + + - + + + +
+
+ Reset zoom to 100% +
+
+ + + + CTRL + + + + + + ALT + + + + + + [ + + + +
+
+ Enter presentation mode +
+
+ + + + ALT + + + + + + F + + + + + + or + + + + + + ALT + + + + + + P + + + +
+
+
+
+ className="canvasKeyboardShortcut" + > +

+ Presentation controls +

+
+
+
+ Enter presentation mode +
+
+ + + + ALT + + + + + + F + + + + + + or + + + + + + ALT + + + + + + P + + + +
+
+ Exit presentation mode +
+
+ + + + ESC + + + +
+
+ Go to previous page +
+
+ + + + ALT + + + + + + [ + + + + + + or + + + + + + BACKSPACE + + + + + + or + + + + + + ← + + + +
+
+ Go to next page +
+
+ + + + ALT + + + + + + ] + + + + + + or + + + + + + SPACE + + + + + + or + + + + + + → + + + +
+
+ Refresh workpad +
+
+ + + + ALT + + + + + + R + + + +
+
+ Toggle page cycling +
+
+ + + + P + + + +
+
+
+
diff --git a/x-pack/legacy/plugins/graph/public/components/field_manager/field_editor.tsx b/x-pack/legacy/plugins/graph/public/components/field_manager/field_editor.tsx index f2a4c28afcdae..9c7cffa775781 100644 --- a/x-pack/legacy/plugins/graph/public/components/field_manager/field_editor.tsx +++ b/x-pack/legacy/plugins/graph/public/components/field_manager/field_editor.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, ButtonHTMLAttributes } from 'react'; import { EuiPopover, EuiFormRow, @@ -23,7 +23,6 @@ import { EuiForm, EuiSpacer, EuiIconTip, - EuiComboBoxOptionProps, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import classNames from 'classnames'; @@ -224,14 +223,12 @@ export function FieldEditor({ }} singleSelection={{ asPlainText: true }} isClearable={false} - options={ - toOptions(allFields, initialField) as Array> - } + options={toOptions(allFields, initialField)} selectedOptions={[ { value: currentField.name, label: currentField.name, - type: currentField.type, + type: currentField.type as ButtonHTMLAttributes['type'], }, ]} renderOption={(option, searchValue, contentClassName) => { @@ -379,12 +376,12 @@ export function FieldEditor({ function toOptions( fields: WorkspaceField[], currentField: WorkspaceField -): Array<{ label: string; value: string; type: string }> { +): Array<{ label: string; value: string; type: ButtonHTMLAttributes['type'] }> { return fields .filter(field => !field.selected || field === currentField) .map(({ name, type }) => ({ label: name, value: name, - type, + type: type as ButtonHTMLAttributes['type'], })); } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index 77435fcdf3eed..8651751ea365b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -7,7 +7,7 @@ import _ from 'lodash'; import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiComboBox, EuiFlexGroup, EuiFlexItem, EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiComboBox, EuiFlexGroup, EuiFlexItem, EuiComboBoxOptionOption } from '@elastic/eui'; import classNames from 'classnames'; import { EuiHighlight } from '@elastic/eui'; import { OperationType } from '../indexpattern'; @@ -138,10 +138,10 @@ export function FieldSelect({ placeholder={i18n.translate('xpack.lens.indexPattern.fieldPlaceholder', { defaultMessage: 'Field', })} - options={(memoizedFieldOptions as unknown) as EuiComboBoxOptionProps[]} + options={(memoizedFieldOptions as unknown) as EuiComboBoxOptionOption[]} isInvalid={Boolean(incompatibleSelectedOperationType && selectedColumnOperationType)} selectedOptions={ - selectedColumnOperationType + ((selectedColumnOperationType ? selectedColumnSourceField ? [ { @@ -150,7 +150,7 @@ export function FieldSelect({ }, ] : [memoizedFieldOptions[0]] - : [] + : []) as unknown) as EuiComboBoxOptionOption[] } singleSelection={{ asPlainText: true }} onChange={choices => { diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index 70722d9cb953a..c744c357c9550 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -8,7 +8,7 @@ import React, { Fragment, FC, useEffect, useMemo } from 'react'; import { EuiComboBox, - EuiComboBoxOptionProps, + EuiComboBoxOptionOption, EuiForm, EuiFieldText, EuiFormRow, @@ -118,7 +118,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta } }; - const onCreateOption = (searchValue: string, flattenedOptions: EuiComboBoxOptionProps[]) => { + const onCreateOption = (searchValue: string, flattenedOptions: EuiComboBoxOptionOption[]) => { const normalizedSearchValue = searchValue.trim().toLowerCase(); if (!normalizedSearchValue) { @@ -132,7 +132,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta // Create the option if it doesn't exist. if ( !flattenedOptions.some( - (option: EuiComboBoxOptionProps) => + (option: EuiComboBoxOptionOption) => option.label.trim().toLowerCase() === normalizedSearchValue ) ) { @@ -164,7 +164,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta // If sourceIndex has changed load analysis field options again if (previousSourceIndex !== sourceIndex || previousJobType !== jobType) { - const analyzedFieldsOptions: EuiComboBoxOptionProps[] = []; + const analyzedFieldsOptions: EuiComboBoxOptionOption[] = []; if (resp.field_selection) { resp.field_selection.forEach((selectedField: FieldSelectionItem) => { @@ -229,7 +229,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta // Get fields and filter for supported types for job type const { fields } = newJobCapsService; - const depVarOptions: EuiComboBoxOptionProps[] = []; + const depVarOptions: EuiComboBoxOptionOption[] = []; fields.forEach((field: Field) => { if (shouldAddAsDepVarOption(field, jobType)) { @@ -276,7 +276,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta return errors; }; - const onSourceIndexChange = (selectedOptions: EuiComboBoxOptionProps[]) => { + const onSourceIndexChange = (selectedOptions: EuiComboBoxOptionOption[]) => { setFormState({ excludes: [], excludesOptions: [], diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 1f23048e09d1f..170700d35e651 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; import { DeepPartial } from '../../../../../../../common/types/common'; import { checkPermission } from '../../../../../privilege/check_privilege'; import { mlNodesAvailable } from '../../../../../ml_nodes_check/check_ml_nodes'; @@ -46,7 +46,7 @@ export interface State { createIndexPattern: boolean; dependentVariable: DependentVariable; dependentVariableFetchFail: boolean; - dependentVariableOptions: EuiComboBoxOptionProps[] | []; + dependentVariableOptions: EuiComboBoxOptionOption[]; description: string; destinationIndex: EsIndexName; destinationIndexNameExists: boolean; @@ -54,7 +54,7 @@ export interface State { destinationIndexNameValid: boolean; destinationIndexPatternTitleExists: boolean; excludes: string[]; - excludesOptions: EuiComboBoxOptionProps[]; + excludesOptions: EuiComboBoxOptionOption[]; fieldOptionsFetchFail: boolean; jobId: DataFrameAnalyticsId; jobIdExists: boolean; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap index 997b437508c34..46428ff9c351a 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap @@ -40,6 +40,7 @@ exports[`Overrides render overrides 1`] = ` labelType="label" > = ({ }); }; - const onQueryEntitiesChange = (selectedOptions: EuiComboBoxOption[]) => { + const onQueryEntitiesChange = (selectedOptions: EuiComboBoxOptionOption[]) => { const selectedFieldNames = selectedOptions.map(option => option.label); const kibanaSettings = customUrl.kibanaSettings; @@ -172,7 +168,7 @@ export const CustomUrlEditor: FC = ({ }); const entityOptions = queryEntityFieldNames.map(fieldName => ({ label: fieldName })); - let selectedEntityOptions: EuiComboBoxOption[] = []; + let selectedEntityOptions: EuiComboBoxOptionOption[] = []; if (kibanaSettings !== undefined && kibanaSettings.queryFieldNames !== undefined) { const queryFieldNames: string[] = kibanaSettings.queryFieldNames; selectedEntityOptions = queryFieldNames.map(fieldName => ({ label: fieldName })); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx index ff6706edb0179..0633c62f754e0 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx @@ -6,7 +6,7 @@ import React, { FC } from 'react'; -import { EuiCodeEditor } from '@elastic/eui'; +import { EuiCodeEditor, EuiCodeEditorProps } from '@elastic/eui'; import { expandLiteralStrings } from '../../../../../../shared_imports'; import { xJsonMode } from '../../../../components/custom_hooks'; @@ -20,7 +20,7 @@ interface MlJobEditorProps { readOnly?: boolean; syntaxChecking?: boolean; theme?: string; - onChange?: Function; + onChange?: EuiCodeEditorProps['onChange']; } export const MLJobEditor: FC = ({ value, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/job_groups_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/job_groups_input.tsx index 7211c034617f1..131e313e7c9e5 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/job_groups_input.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/components/job_groups_input.tsx @@ -6,7 +6,7 @@ import React, { FC, memo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { Validation } from '../job_validator'; import { tabColor } from '../../../../../../common/util/group_color_utils'; import { Description } from '../../pages/components/job_details_step/components/groups/description'; @@ -20,28 +20,28 @@ export interface JobGroupsInputProps { export const JobGroupsInput: FC = memo( ({ existingGroups, selectedGroups, onChange, validation }) => { - const options = existingGroups.map(g => ({ + const options = existingGroups.map(g => ({ label: g, color: tabColor(g), })); - const selectedOptions = selectedGroups.map(g => ({ + const selectedOptions = selectedGroups.map(g => ({ label: g, color: tabColor(g), })); - function onChangeCallback(optionsIn: EuiComboBoxOptionProps[]) { + function onChangeCallback(optionsIn: EuiComboBoxOptionOption[]) { onChange(optionsIn.map(g => g.label)); } - function onCreateGroup(input: string, flattenedOptions: EuiComboBoxOptionProps[]) { + function onCreateGroup(input: string, flattenedOptions: EuiComboBoxOptionOption[]) { const normalizedSearchValue = input.trim().toLowerCase(); if (!normalizedSearchValue) { return; } - const newGroup: EuiComboBoxOptionProps = { + const newGroup: EuiComboBoxOptionOption = { label: input, color: tabColor(input), }; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx index 9af1226d1fe6c..869dc046648b3 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/time_field/time_field_select.tsx @@ -5,7 +5,7 @@ */ import React, { FC, useContext } from 'react'; -import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { Field } from '../../../../../../../../../common/types/fields'; @@ -19,14 +19,17 @@ interface Props { export const TimeFieldSelect: FC = ({ fields, changeHandler, selectedField }) => { const { jobCreator } = useContext(JobCreatorContext); - const options: EuiComboBoxOptionProps[] = createFieldOptions(fields, jobCreator.additionalFields); + const options: EuiComboBoxOptionOption[] = createFieldOptions( + fields, + jobCreator.additionalFields + ); - const selection: EuiComboBoxOptionProps[] = []; + const selection: EuiComboBoxOptionOption[] = []; if (selectedField !== null) { selection.push({ label: selectedField }); } - function onChange(selectedOptions: EuiComboBoxOptionProps[]) { + function onChange(selectedOptions: EuiComboBoxOptionOption[]) { const option = selectedOptions[0]; if (typeof option !== 'undefined') { changeHandler(option.label); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx index 1e7327552623e..597fe42543301 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/calendars/calendars_selection.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonIcon, EuiComboBox, - EuiComboBoxOptionProps, + EuiComboBoxOptionOption, EuiComboBoxProps, EuiFlexGroup, EuiFlexItem, @@ -28,10 +28,10 @@ import { GLOBAL_CALENDAR } from '../../../../../../../../../../../common/constan export const CalendarsSelection: FC = () => { const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); const [selectedCalendars, setSelectedCalendars] = useState(jobCreator.calendars); - const [selectedOptions, setSelectedOptions] = useState>>( + const [selectedOptions, setSelectedOptions] = useState>>( [] ); - const [options, setOptions] = useState>>([]); + const [options, setOptions] = useState>>([]); const [isLoading, setIsLoading] = useState(false); async function loadCalendars() { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx index cf0be9d3c0c4e..841ccfdce0958 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx @@ -5,7 +5,7 @@ */ import React, { FC, useState, useContext, useEffect } from 'react'; -import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { JobCreatorContext } from '../../../job_creator_context'; import { tabColor } from '../../../../../../../../../common/util/group_color_utils'; @@ -24,28 +24,28 @@ export const GroupsInput: FC = () => { jobCreatorUpdate(); }, [selectedGroups.join()]); - const options: EuiComboBoxOptionProps[] = existingJobsAndGroups.groupIds.map((g: string) => ({ + const options: EuiComboBoxOptionOption[] = existingJobsAndGroups.groupIds.map((g: string) => ({ label: g, color: tabColor(g), })); - const selectedOptions: EuiComboBoxOptionProps[] = selectedGroups.map((g: string) => ({ + const selectedOptions: EuiComboBoxOptionOption[] = selectedGroups.map((g: string) => ({ label: g, color: tabColor(g), })); - function onChange(optionsIn: EuiComboBoxOptionProps[]) { + function onChange(optionsIn: EuiComboBoxOptionOption[]) { setSelectedGroups(optionsIn.map(g => g.label)); } - function onCreateGroup(input: string, flattenedOptions: EuiComboBoxOptionProps[]) { + function onCreateGroup(input: string, flattenedOptions: EuiComboBoxOptionOption[]) { const normalizedSearchValue = input.trim().toLowerCase(); if (!normalizedSearchValue) { return; } - const newGroup: EuiComboBoxOptionProps = { + const newGroup: EuiComboBoxOptionOption = { label: input, color: tabColor(input), }; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx index 753cea7adcb35..9e784a20c4f5f 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx @@ -10,7 +10,7 @@ import { EuiFlexItem, EuiFlexGroup, EuiFlexGrid, - EuiComboBoxOptionProps, + EuiComboBoxOptionOption, EuiHorizontalRule, EuiTextArea, } from '@elastic/eui'; @@ -54,11 +54,11 @@ export interface ModalPayload { index?: number; } -const emptyOption: EuiComboBoxOptionProps = { +const emptyOption: EuiComboBoxOptionOption = { label: '', }; -const excludeFrequentOptions: EuiComboBoxOptionProps[] = [{ label: 'all' }, { label: 'none' }]; +const excludeFrequentOptions: EuiComboBoxOptionOption[] = [{ label: 'all' }, { label: 'none' }]; export const AdvancedDetectorModal: FC = ({ payload, @@ -90,7 +90,7 @@ export const AdvancedDetectorModal: FC = ({ const usingScriptFields = jobCreator.additionalFields.length > 0; // list of aggregation combobox options. - const aggOptions: EuiComboBoxOptionProps[] = aggs + const aggOptions: EuiComboBoxOptionOption[] = aggs .filter(agg => filterAggs(agg, usingScriptFields)) .map(createAggOption); @@ -101,19 +101,19 @@ export const AdvancedDetectorModal: FC = ({ fields ); - const allFieldOptions: EuiComboBoxOptionProps[] = [ + const allFieldOptions: EuiComboBoxOptionOption[] = [ ...createFieldOptions(fields, jobCreator.additionalFields), ].sort(comboBoxOptionsSort); - const splitFieldOptions: EuiComboBoxOptionProps[] = [ + const splitFieldOptions: EuiComboBoxOptionOption[] = [ ...allFieldOptions, ...createMlcategoryFieldOption(jobCreator.categorizationFieldName), ].sort(comboBoxOptionsSort); const eventRateField = fields.find(f => f.id === EVENT_RATE_FIELD_ID); - const onOptionChange = (func: (p: EuiComboBoxOptionProps) => any) => ( - selectedOptions: EuiComboBoxOptionProps[] + const onOptionChange = (func: (p: EuiComboBoxOptionOption) => any) => ( + selectedOptions: EuiComboBoxOptionOption[] ) => { func(selectedOptions[0] || emptyOption); }; @@ -312,7 +312,7 @@ export const AdvancedDetectorModal: FC = ({ ); }; -function createAggOption(agg: Aggregation | null): EuiComboBoxOptionProps { +function createAggOption(agg: Aggregation | null): EuiComboBoxOptionOption { if (agg === null) { return emptyOption; } @@ -328,7 +328,7 @@ function filterAggs(agg: Aggregation, usingScriptFields: boolean) { return agg.fields !== undefined && (usingScriptFields || agg.fields.length); } -function createFieldOption(field: Field | null): EuiComboBoxOptionProps { +function createFieldOption(field: Field | null): EuiComboBoxOptionOption { if (field === null) { return emptyOption; } @@ -337,7 +337,7 @@ function createFieldOption(field: Field | null): EuiComboBoxOptionProps { }; } -function createExcludeFrequentOption(excludeFrequent: string | null): EuiComboBoxOptionProps { +function createExcludeFrequentOption(excludeFrequent: string | null): EuiComboBoxOptionOption { if (excludeFrequent === null) { return emptyOption; } @@ -406,15 +406,15 @@ function createDefaultDescription(dtr: RichDetector) { // if the options list only contains one option and nothing has been selected, set // selectedOptions list to be an empty array function createSelectedOptions( - selectedOption: EuiComboBoxOptionProps, - options: EuiComboBoxOptionProps[] -): EuiComboBoxOptionProps[] { + selectedOption: EuiComboBoxOptionOption, + options: EuiComboBoxOptionOption[] +): EuiComboBoxOptionOption[] { return (options.length === 1 && options[0].label !== selectedOption.label) || selectedOption.label === '' ? [] : [selectedOption]; } -function comboBoxOptionsSort(a: EuiComboBoxOptionProps, b: EuiComboBoxOptionProps) { +function comboBoxOptionsSort(a: EuiComboBoxOptionOption, b: EuiComboBoxOptionOption) { return a.label.localeCompare(b.label); } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx index a2434f3c33559..e4eccb5f01423 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx @@ -5,7 +5,7 @@ */ import React, { FC, useContext, useState, useEffect } from 'react'; -import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { Field, Aggregation, AggFieldPair } from '../../../../../../../../../common/types/fields'; @@ -26,12 +26,12 @@ export interface DropDownOption { options: DropDownLabel[]; } -export type DropDownProps = DropDownLabel[] | EuiComboBoxOptionProps[]; +export type DropDownProps = DropDownLabel[] | EuiComboBoxOptionOption[]; interface Props { fields: Field[]; - changeHandler(d: EuiComboBoxOptionProps[]): void; - selectedOptions: EuiComboBoxOptionProps[]; + changeHandler(d: EuiComboBoxOptionOption[]): void; + selectedOptions: EuiComboBoxOptionOption[]; removeOptions: AggFieldPair[]; } @@ -42,7 +42,7 @@ export const AggSelect: FC = ({ fields, changeHandler, selectedOptions, r // so they can be removed from the dropdown list const removeLabels = removeOptions.map(createLabel); - const options: EuiComboBoxOptionProps[] = fields.map(f => { + const options: EuiComboBoxOptionOption[] = fields.map(f => { const aggOption: DropDownOption = { label: f.name, options: [] }; if (typeof f.aggs !== 'undefined') { aggOption.options = f.aggs diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx index 6451c2785eae0..2f3e8d43bc169 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx @@ -5,7 +5,7 @@ */ import React, { FC, useContext } from 'react'; -import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { Field } from '../../../../../../../../../common/types/fields'; @@ -19,16 +19,16 @@ interface Props { export const CategorizationFieldSelect: FC = ({ fields, changeHandler, selectedField }) => { const { jobCreator } = useContext(JobCreatorContext); - const options: EuiComboBoxOptionProps[] = [ + const options: EuiComboBoxOptionOption[] = [ ...createFieldOptions(fields, jobCreator.additionalFields), ]; - const selection: EuiComboBoxOptionProps[] = []; + const selection: EuiComboBoxOptionOption[] = []; if (selectedField !== null) { selection.push({ label: selectedField }); } - function onChange(selectedOptions: EuiComboBoxOptionProps[]) { + function onChange(selectedOptions: EuiComboBoxOptionOption[]) { const option = selectedOptions[0]; if (typeof option !== 'undefined') { changeHandler(option.label); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx index d4ac470f4ea4f..25c924ee0b42f 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx @@ -5,7 +5,7 @@ */ import React, { FC, useContext } from 'react'; -import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { Field } from '../../../../../../../../../common/types/fields'; @@ -22,14 +22,14 @@ interface Props { export const InfluencersSelect: FC = ({ fields, changeHandler, selectedInfluencers }) => { const { jobCreator } = useContext(JobCreatorContext); - const options: EuiComboBoxOptionProps[] = [ + const options: EuiComboBoxOptionOption[] = [ ...createFieldOptions(fields, jobCreator.additionalFields), ...createMlcategoryFieldOption(jobCreator.categorizationFieldName), ]; - const selection: EuiComboBoxOptionProps[] = selectedInfluencers.map(i => ({ label: i })); + const selection: EuiComboBoxOptionOption[] = selectedInfluencers.map(i => ({ label: i })); - function onChange(selectedOptions: EuiComboBoxOptionProps[]) { + function onChange(selectedOptions: EuiComboBoxOptionOption[]) { changeHandler(selectedOptions.map(o => o.label)); } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx index 378c088332ed4..816614fb2a772 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/split_field_select.tsx @@ -5,7 +5,7 @@ */ import React, { FC } from 'react'; -import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { Field, SplitField } from '../../../../../../../../../common/types/fields'; @@ -31,7 +31,7 @@ export const SplitFieldSelect: FC = ({ testSubject, placeholder, }) => { - const options: EuiComboBoxOptionProps[] = fields.map( + const options: EuiComboBoxOptionOption[] = fields.map( f => ({ label: f.name, @@ -39,12 +39,12 @@ export const SplitFieldSelect: FC = ({ } as DropDownLabel) ); - const selection: EuiComboBoxOptionProps[] = []; + const selection: EuiComboBoxOptionOption[] = []; if (selectedField !== null) { selection.push({ label: selectedField.name, field: selectedField } as DropDownLabel); } - function onChange(selectedOptions: EuiComboBoxOptionProps[]) { + function onChange(selectedOptions: EuiComboBoxOptionOption[]) { const option = selectedOptions[0] as DropDownLabel; if (typeof option !== 'undefined') { changeHandler(option.field); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx index 6fe3aaf0a8652..8136008dce11b 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx @@ -5,7 +5,7 @@ */ import React, { FC, useContext } from 'react'; -import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { Field } from '../../../../../../../../../common/types/fields'; @@ -22,17 +22,17 @@ interface Props { export const SummaryCountFieldSelect: FC = ({ fields, changeHandler, selectedField }) => { const { jobCreator } = useContext(JobCreatorContext); - const options: EuiComboBoxOptionProps[] = [ + const options: EuiComboBoxOptionOption[] = [ ...createFieldOptions(fields, jobCreator.additionalFields), ...createDocCountFieldOption(jobCreator.aggregationFields.length > 0), ]; - const selection: EuiComboBoxOptionProps[] = []; + const selection: EuiComboBoxOptionOption[] = []; if (selectedField !== null) { selection.push({ label: selectedField }); } - function onChange(selectedOptions: EuiComboBoxOptionProps[]) { + function onChange(selectedOptions: EuiComboBoxOptionOption[]) { const option = selectedOptions[0]; if (typeof option !== 'undefined') { changeHandler(option.label); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx index 6727102f55a52..8911ed53e74d0 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EuiComboBox, - EuiComboBoxOptionProps, + EuiComboBoxOptionOption, EuiFlexItem, EuiFormRow, EuiToolTip, @@ -29,13 +29,13 @@ interface EntityControlProps { isLoading: boolean; onSearchChange: (entity: Entity, queryTerm: string) => void; forceSelection: boolean; - options: EuiComboBoxOptionProps[]; + options: EuiComboBoxOptionOption[]; } interface EntityControlState { - selectedOptions: EuiComboBoxOptionProps[] | undefined; + selectedOptions: EuiComboBoxOptionOption[] | undefined; isLoading: boolean; - options: EuiComboBoxOptionProps[] | undefined; + options: EuiComboBoxOptionOption[] | undefined; } export class EntityControl extends Component { @@ -53,7 +53,7 @@ export class EntityControl extends Component 0) || (Array.isArray(selectedOptions) && @@ -84,7 +84,7 @@ export class EntityControl extends Component { + onChange = (selectedOptions: EuiComboBoxOptionOption[]) => { const options = selectedOptions.length > 0 ? selectedOptions : undefined; this.setState({ selectedOptions: options, diff --git a/x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap b/x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap index 2055afdcf2bfe..f89e90cc4860c 100644 --- a/x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap +++ b/x-pack/legacy/plugins/reporting/public/components/__snapshots__/report_info_button.test.tsx.snap @@ -182,9 +182,13 @@ Array [ class="euiFlyoutBody__overflow" >
- Could not fetch the job info +
+ Could not fetch the job info +
@@ -243,9 +247,13 @@ Array [ class="euiFlyoutBody__overflow" >
- Could not fetch the job info +
+ Could not fetch the job info +
@@ -332,13 +340,17 @@ Array [
- -
- Could not fetch the job info -
-
+
+ +
+ Could not fetch the job info +
+
+
@@ -440,13 +452,17 @@ Array [
- -
- Could not fetch the job info -
-
+
+ +
+ Could not fetch the job info +
+
+
@@ -599,8 +615,12 @@ Array [ class="euiFlyoutBody__overflow" >
+ class="euiFlyoutBody__overflowContent" + > +
+
@@ -658,8 +678,12 @@ Array [ class="euiFlyoutBody__overflow" >
+ class="euiFlyoutBody__overflowContent" + > +
+
@@ -745,11 +769,15 @@ Array [
- -
- +
+ +
+ +
@@ -851,11 +879,15 @@ Array [
- -
- +
+ +
+ +
diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/helpers.tsx index 1b003f1336406..e6afc86a7ee67 100644 --- a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/helpers.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { findIndex } from 'lodash/fp'; -import { EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; import { BrowserField, BrowserFields, getAllFieldsByName } from '../../containers/source'; import { @@ -16,7 +16,7 @@ import { import * as i18n from './translations'; /** The list of operators to display in the `Operator` select */ -export const operatorLabels: EuiComboBoxOptionProps[] = [ +export const operatorLabels: EuiComboBoxOptionOption[] = [ { label: i18n.IS, }, @@ -38,7 +38,7 @@ export const getFieldNames = (category: Partial): string[] => : []; /** Returns all field names by category, for display in an `EuiComboBox` */ -export const getCategorizedFieldNames = (browserFields: BrowserFields): EuiComboBoxOptionProps[] => +export const getCategorizedFieldNames = (browserFields: BrowserFields): EuiComboBoxOptionOption[] => Object.keys(browserFields) .sort() .map(categoryId => ({ @@ -55,8 +55,8 @@ export const selectionsAreValid = ({ selectedOperator, }: { browserFields: BrowserFields; - selectedField: EuiComboBoxOptionProps[]; - selectedOperator: EuiComboBoxOptionProps[]; + selectedField: EuiComboBoxOptionOption[]; + selectedOperator: EuiComboBoxOptionOption[]; }): boolean => { const fieldId = selectedField.length > 0 ? selectedField[0].label : ''; const operator = selectedOperator.length > 0 ? selectedOperator[0].label : ''; @@ -69,7 +69,7 @@ export const selectionsAreValid = ({ /** Returns a `QueryOperator` based on the user's Operator selection */ export const getQueryOperatorFromSelection = ( - selectedOperator: EuiComboBoxOptionProps[] + selectedOperator: EuiComboBoxOptionOption[] ): QueryOperator => { const selection = selectedOperator.length > 0 ? selectedOperator[0].label : ''; @@ -88,7 +88,7 @@ export const getQueryOperatorFromSelection = ( /** * Returns `true` when the search excludes results that match the specified data provider */ -export const getExcludedFromSelection = (selectedOperator: EuiComboBoxOptionProps[]): boolean => { +export const getExcludedFromSelection = (selectedOperator: EuiComboBoxOptionOption[]): boolean => { const selection = selectedOperator.length > 0 ? selectedOperator[0].label : ''; switch (selection) { diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx index 87e83e0c47b6d..5ecc96187532d 100644 --- a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx @@ -8,7 +8,7 @@ import { noop } from 'lodash/fp'; import { EuiButton, EuiComboBox, - EuiComboBoxOptionProps, + EuiComboBoxOptionOption, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -64,7 +64,7 @@ const sanatizeValue = (value: string | number): string => export const getInitialOperatorLabel = ( isExcluded: boolean, operator: QueryOperator -): EuiComboBoxOptionProps[] => { +): EuiComboBoxOptionOption[] => { if (operator === ':') { return isExcluded ? [{ label: i18n.IS_NOT }] : [{ label: i18n.IS }]; } else { @@ -84,8 +84,8 @@ export const StatefulEditDataProvider = React.memo( timelineId, value, }) => { - const [updatedField, setUpdatedField] = useState([{ label: field }]); - const [updatedOperator, setUpdatedOperator] = useState( + const [updatedField, setUpdatedField] = useState([{ label: field }]); + const [updatedOperator, setUpdatedOperator] = useState( getInitialOperatorLabel(isExcluded, operator) ); const [updatedValue, setUpdatedValue] = useState(value); @@ -105,13 +105,13 @@ export const StatefulEditDataProvider = React.memo( } }; - const onFieldSelected = useCallback((selectedField: EuiComboBoxOptionProps[]) => { + const onFieldSelected = useCallback((selectedField: EuiComboBoxOptionOption[]) => { setUpdatedField(selectedField); focusInput(); }, []); - const onOperatorSelected = useCallback((operatorSelected: EuiComboBoxOptionProps[]) => { + const onOperatorSelected = useCallback((operatorSelected: EuiComboBoxOptionOption[]) => { setUpdatedOperator(operatorSelected); focusInput(); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx index b8280aedd12fa..be83a4f7b33a7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx @@ -16,8 +16,8 @@ import { EuiFilterButton, EuiFilterGroup, EuiPortal, + EuiSelectableOption, } from '@elastic/eui'; -import { Option } from '@elastic/eui/src/components/selectable/types'; import { isEmpty } from 'lodash/fp'; import React, { memo, useCallback, useMemo, useState } from 'react'; import { ListProps } from 'react-virtualized'; @@ -91,10 +91,10 @@ const getBasicSelectableOptions = (timelineId: string) => [ description: i18n.DEFAULT_TIMELINE_DESCRIPTION, favorite: [], label: i18n.DEFAULT_TIMELINE_TITLE, - id: null, + id: undefined, title: i18n.DEFAULT_TIMELINE_TITLE, checked: timelineId === '-1' ? 'on' : undefined, - } as Option, + } as EuiSelectableOption, ]; const ORIGINAL_PAGE_SIZE = 50; @@ -326,7 +326,7 @@ const SearchTimelineSuperSelectComponent: React.FC diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index 2214190de6a16..8cbad4e89c106 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -42,7 +42,7 @@ export const getActions = ( ) => [ { description: i18n.EDIT_RULE_SETTINGS, - icon: 'visControls', + icon: 'controlsHorizontal', name: i18n.EDIT_RULE_SETTINGS, onClick: (rowItem: Rule) => editRuleAction(rowItem, history), enabled: (rowItem: Rule) => !rowItem.immutable, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx index 9a68797aea79b..97649fb03dac0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx @@ -113,8 +113,8 @@ export const ImportRuleModalComponent = ({ { - setSelectedFiles(Object.keys(files).length > 0 ? files : null); + onChange={(files: FileList | null) => { + setSelectedFiles(files && files.length > 0 ? files : null); }} display={'large'} fullWidth={true} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 83dd18f0f14b7..cd255b0951597 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -274,7 +274,7 @@ const RuleDetailsPageComponent: FC = ({ {ruleI18n.EDIT_RULE_SETTINGS} diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx index 9ff235fb40d8a..157e0f76856c8 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx @@ -6,12 +6,12 @@ import React from 'react'; -import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; interface Props { - options: EuiComboBoxOptionProps[]; + options: EuiComboBoxOptionOption[]; placeholder?: string; - changeHandler(d: EuiComboBoxOptionProps[]): void; + changeHandler(d: EuiComboBoxOptionOption[]): void; testSubj?: string; } diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts index 7b78d4ffccfa1..35e1ea02a5cef 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; -import { EuiComboBoxOptionProps, EuiDataGridSorting } from '@elastic/eui'; +import { EuiComboBoxOptionOption, EuiDataGridSorting } from '@elastic/eui'; import { IndexPattern, KBN_FIELD_TYPES, @@ -112,11 +112,11 @@ const illegalEsAggNameChars = /[[\]>]/g; export function getPivotDropdownOptions(indexPattern: IndexPattern) { // The available group by options - const groupByOptions: EuiComboBoxOptionProps[] = []; + const groupByOptions: EuiComboBoxOptionOption[] = []; const groupByOptionsData: PivotGroupByConfigWithUiSupportDict = {}; // The available aggregations - const aggOptions: EuiComboBoxOptionProps[] = []; + const aggOptions: EuiComboBoxOptionOption[] = []; const aggOptionsData: PivotAggsConfigWithUiSupportDict = {}; const ignoreFieldNames = ['_id', '_index', '_type']; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/ping_list.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/ping_list.test.tsx index ba07d6c63b36c..7705c72fa14a0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/ping_list.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/ping_list.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { PingResults, Ping } from '../../../../../common/graphql/types'; import { PingListComponent, AllLocationOption, toggleDetails } from '../ping_list'; -import { EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; import { ExpandedRowMap } from '../../monitor_list/types'; describe('PingList component', () => { @@ -205,7 +205,7 @@ describe('PingList component', () => { loading={false} data={{ allPings }} onPageCountChange={jest.fn()} - onSelectedLocationChange={(loc: EuiComboBoxOptionProps[]) => {}} + onSelectedLocationChange={(loc: EuiComboBoxOptionOption[]) => {}} onSelectedStatusChange={jest.fn()} pageSize={30} selectedOption="down" diff --git a/x-pack/package.json b/x-pack/package.json index 585d05b3c8a13..11068bcccf561 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -179,7 +179,7 @@ "@elastic/apm-rum-react": "^0.3.2", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "7.6.0", - "@elastic/eui": "19.0.0", + "@elastic/eui": "20.0.2", "@elastic/filesaver": "1.1.2", "@elastic/maki": "6.1.0", "@elastic/node-crypto": "^1.0.0", diff --git a/x-pack/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx b/x-pack/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx index 0835a904585ed..3c96d505dce4d 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx @@ -5,7 +5,7 @@ */ import { EuiBadge, EuiButton, EuiPopover, EuiPopoverTitle, EuiSelectable } from '@elastic/eui'; -import { Option } from '@elastic/eui/src/components/selectable/types'; +import { EuiSelectableOption } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback, useMemo } from 'react'; import { v4 as uuidv4 } from 'uuid'; @@ -15,7 +15,7 @@ import { useVisibilityState } from '../../utils/use_visibility_state'; import { euiStyled } from '../../../../observability/public'; interface SelectableColumnOption { - optionProps: Option; + optionProps: EuiSelectableOption; columnConfiguration: LogColumnConfiguration; } @@ -78,13 +78,13 @@ export const AddLogColumnButtonAndPopover: React.FunctionComponent<{ [availableFields] ); - const availableOptions = useMemo( + const availableOptions = useMemo( () => availableColumnOptions.map(availableColumnOption => availableColumnOption.optionProps), [availableColumnOptions] ); const handleColumnSelection = useCallback( - (selectedOptions: Option[]) => { + (selectedOptions: EuiSelectableOption[]) => { closePopover(); const selectedOptionIndex = selectedOptions.findIndex( diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_selector.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_selector.tsx index 9c22caa4b3465..c2087e9032f59 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_selector.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/datasets_selector.tsx @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useMemo } from 'react'; import { getFriendlyNameForPartitionId } from '../../../../../../common/log_analysis'; -type DatasetOptionProps = EuiComboBoxOptionProps; +type DatasetOptionProps = EuiComboBoxOptionOption; export const DatasetsSelector: React.FunctionComponent<{ availableDatasets: string[]; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap index 45751997eb0d5..590ea27617adf 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap @@ -165,6 +165,7 @@ Array [ style="font-size:14px;display:inline-block" > @@ -473,6 +474,7 @@ Array [ style="font-size: 14px; display: inline-block;" > ; + option: EuiComboBoxOptionOption<{ isDeprecated: boolean }>; } export const RoleComboBoxOption = ({ option }: Props) => { diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx index 43f6c50ea1172..c5b3ea433adaa 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx @@ -55,7 +55,7 @@ describe('JSONRuleEditor', () => { const wrapper = mountWithIntl(); const { value } = wrapper.find(EuiCodeEditor).props(); - expect(JSON.parse(value)).toEqual({ + expect(JSON.parse(value as string)).toEqual({ all: [ { any: [{ field: { username: '*' } }], @@ -90,10 +90,7 @@ describe('JSONRuleEditor', () => { const allRule = JSON.stringify(new AllRule().toRaw()); act(() => { - wrapper - .find(EuiCodeEditor) - .props() - .onChange(allRule + ', this makes invalid JSON'); + wrapper.find(EuiCodeEditor).props().onChange!(allRule + ', this makes invalid JSON'); }); expect(props.onValidityChange).toHaveBeenCalledTimes(1); @@ -121,10 +118,7 @@ describe('JSONRuleEditor', () => { }); act(() => { - wrapper - .find(EuiCodeEditor) - .props() - .onChange(invalidRule); + wrapper.find(EuiCodeEditor).props().onChange!(invalidRule); }); expect(props.onValidityChange).toHaveBeenCalledTimes(1); @@ -143,10 +137,7 @@ describe('JSONRuleEditor', () => { const allRule = JSON.stringify(new AllRule().toRaw()); act(() => { - wrapper - .find(EuiCodeEditor) - .props() - .onChange(allRule + ', this makes invalid JSON'); + wrapper.find(EuiCodeEditor).props().onChange!(allRule + ', this makes invalid JSON'); }); expect(props.onValidityChange).toHaveBeenCalledTimes(1); @@ -156,10 +147,7 @@ describe('JSONRuleEditor', () => { props.onValidityChange.mockReset(); act(() => { - wrapper - .find(EuiCodeEditor) - .props() - .onChange(allRule); + wrapper.find(EuiCodeEditor).props().onChange!(allRule); }); expect(props.onValidityChange).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap index b38b7e6634ada..a52438ca93638 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap @@ -6,6 +6,7 @@ exports[`it renders without crashing 1`] = ` key="clusterPrivs" > { }); }; - private onIndexPatternsChange = (newPatterns: EuiComboBoxOptionProps[]) => { + private onIndexPatternsChange = (newPatterns: EuiComboBoxOptionOption[]) => { this.props.onChange({ ...this.props.indexPrivilege, names: newPatterns.map(fromOption), }); }; - private onPrivilegeChange = (newPrivileges: EuiComboBoxOptionProps[]) => { + private onPrivilegeChange = (newPrivileges: EuiComboBoxOptionOption[]) => { this.props.onChange({ ...this.props.indexPrivilege, privileges: newPrivileges.map(fromOption), @@ -418,7 +418,7 @@ export class IndexPrivilegeForm extends Component { }); }; - private onGrantedFieldsChange = (grantedFields: EuiComboBoxOptionProps[]) => { + private onGrantedFieldsChange = (grantedFields: EuiComboBoxOptionOption[]) => { this.props.onChange({ ...this.props.indexPrivilege, field_security: { @@ -447,7 +447,7 @@ export class IndexPrivilegeForm extends Component { }); }; - private onDeniedFieldsChange = (deniedFields: EuiComboBoxOptionProps[]) => { + private onDeniedFieldsChange = (deniedFields: EuiComboBoxOptionOption[]) => { this.props.onChange({ ...this.props.indexPrivilege, field_security: { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx index 3e5ea9f146876..1e42a926c51f7 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiComboBox, EuiComboBoxOptionProps, EuiHealth, EuiHighlight } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiHealth, EuiHighlight } from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n/react'; import React, { Component } from 'react'; import { Space, getSpaceColor } from '../../../../../../../../spaces/public'; @@ -65,7 +65,7 @@ export class SpaceSelector extends Component { ); } - private onChange = (selectedSpaces: EuiComboBoxOptionProps[]) => { + private onChange = (selectedSpaces: EuiComboBoxOptionOption[]) => { this.props.onChange(selectedSpaces.map(s => (s.id as string).split('spaceOption_')[1])); }; @@ -81,12 +81,12 @@ export class SpaceSelector extends Component { ) ); - return options.filter(Boolean) as EuiComboBoxOptionProps[]; + return options.filter(Boolean) as EuiComboBoxOptionOption[]; }; private getSelectedOptions = () => { const options = this.props.selectedSpaceIds.map(spaceIdToOption(this.props.spaces)); - return options.filter(Boolean) as EuiComboBoxOptionProps[]; + return options.filter(Boolean) as EuiComboBoxOptionOption[]; }; } diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings.tsx index 45eea10a28311..fc743767e9f70 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings.tsx @@ -20,7 +20,7 @@ import { EuiComboBox, EuiToolTip, } from '@elastic/eui'; -import { Option } from '@elastic/eui/src/components/selectable/types'; +import { EuiSelectableOption } from '@elastic/eui'; import { SlmPolicyPayload, SnapshotConfig } from '../../../../../common/types'; import { documentationLinksService } from '../../../services/documentation'; import { useServices } from '../../../app_context'; @@ -45,9 +45,9 @@ export const PolicyStepSettings: React.FunctionComponent = ({ // States for choosing all indices, or a subset, including caching previously chosen subset list const [isAllIndices, setIsAllIndices] = useState(!Boolean(config.indices)); const [indicesSelection, setIndicesSelection] = useState([...indices]); - const [indicesOptions, setIndicesOptions] = useState( + const [indicesOptions, setIndicesOptions] = useState( indices.map( - (index): Option => ({ + (index): EuiSelectableOption => ({ label: index, checked: isAllIndices || @@ -210,7 +210,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({ data-test-subj="deselectIndicesLink" onClick={() => { // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed - indicesOptions.forEach((option: Option) => { + indicesOptions.forEach((option: EuiSelectableOption) => { option.checked = undefined; }); updatePolicyConfig({ indices: [] }); @@ -226,7 +226,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({ { // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed - indicesOptions.forEach((option: Option) => { + indicesOptions.forEach((option: EuiSelectableOption) => { option.checked = 'on'; }); updatePolicyConfig({ indices: [...indices] }); diff --git a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/hdfs_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/hdfs_settings.tsx index c504cccf0ac4b..6d936f41206cc 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/hdfs_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/hdfs_settings.tsx @@ -5,6 +5,7 @@ */ import React, { Fragment, useState } from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCode, @@ -391,15 +392,13 @@ export const HDFSSettings: React.FunctionComponent = ({ }} showGutter={false} minLines={6} - aria-label={ - - } + aria-label={i18n.translate( + 'xpack.snapshotRestore.repositoryForm.typeHDFS.configurationAriaLabel', + { + defaultMessage: `Additional configuration for HDFS repository '{name}'`, + values: { name }, + } + )} onChange={(value: string) => { setAdditionalConf(value); try { diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx index 6780ab4bc664e..0896b283a6762 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx @@ -20,7 +20,7 @@ import { EuiTitle, EuiComboBox, } from '@elastic/eui'; -import { Option } from '@elastic/eui/src/components/selectable/types'; +import { EuiSelectableOption } from '@elastic/eui'; import { RestoreSettings } from '../../../../../common/types'; import { documentationLinksService } from '../../../services/documentation'; import { useServices } from '../../../app_context'; @@ -48,9 +48,9 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = // States for choosing all indices, or a subset, including caching previously chosen subset list const [isAllIndices, setIsAllIndices] = useState(!Boolean(restoreIndices)); - const [indicesOptions, setIndicesOptions] = useState( + const [indicesOptions, setIndicesOptions] = useState( snapshotIndices.map( - (index): Option => ({ + (index): EuiSelectableOption => ({ label: index, checked: isAllIndices || @@ -230,7 +230,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = { // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed - indicesOptions.forEach((option: Option) => { + indicesOptions.forEach((option: EuiSelectableOption) => { option.checked = undefined; }); updateRestoreSettings({ indices: [] }); @@ -249,7 +249,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = { // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed - indicesOptions.forEach((option: Option) => { + indicesOptions.forEach((option: EuiSelectableOption) => { option.checked = 'on'; }); updateRestoreSettings({ indices: [...snapshotIndices] }); diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx index 3f7daea361f7f..52d162d0963f3 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx @@ -282,12 +282,10 @@ export const RestoreSnapshotStepReview: React.FunctionComponent = ({ setOptions={{ maxLines: Infinity }} value={JSON.stringify(serializedRestoreSettings, null, 2)} editorProps={{ $blockScrolling: Infinity }} - aria-label={ - - } + aria-label={i18n.translate( + 'xpack.snapshotRestore.restoreForm.stepReview.jsonTab.jsonAriaLabel', + { defaultMessage: 'Restore settings to be executed' } + )} /> ); diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx index fd29fc3105f90..d9a5a06d862d6 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx @@ -183,12 +183,10 @@ export const RestoreSnapshotStepSettings: React.FunctionComponent = ( showGutter={false} minLines={6} maxLines={15} - aria-label={ - - } + aria-label={i18n.translate( + 'xpack.snapshotRestore.restoreForm.stepSettings.indexSettingsAriaLabel', + { defaultMessage: 'Index settings to modify' } + )} onChange={(value: string) => { updateRestoreSettings({ indexSettings: value, diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_history.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_history.tsx index 708042359d088..22c37241348e7 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_history.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_history.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCodeEditor, @@ -155,15 +156,13 @@ export const TabHistory: React.FunctionComponent = ({ policy }) => { maxLines={12} wrapEnabled={true} showGutter={false} - aria-label={ - - } + aria-label={i18n.translate( + 'xpack.snapshotRestore.policyDetails.lastFailure.detailsAriaLabel', + { + defaultMessage: `Last failure details for policy '{name}'`, + values: { name }, + } + )} /> diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/type_details/default_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/type_details/default_details.tsx index 6b99628863e77..80bf9fdee24e1 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/type_details/default_details.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/type_details/default_details.tsx @@ -6,6 +6,7 @@ import 'brace/theme/textmate'; import React, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCodeEditor, EuiSpacer, EuiTitle } from '@elastic/eui'; @@ -47,15 +48,15 @@ export const DefaultDetails: React.FunctionComponent = ({ }} showGutter={false} minLines={6} - aria-label={ - - } + }, + } + )} /> ); diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap b/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap index 562641d8fca51..269b2b6908183 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap @@ -53,14 +53,7 @@ exports[`renders without crashing 1`] = ` labelType="label" > { image.src = imgUrl; }; - private onFileUpload = (files: File[]) => { - const [file] = files; + private onFileUpload = (files: FileList | null) => { + if (files == null) return; + const file = files[0]; if (imageTypes.indexOf(file.type) > -1) { encode(file).then((dataurl: string) => this.handleImageUpload(dataurl)); } @@ -169,7 +170,7 @@ export class CustomizeSpaceAvatar extends Component { } )} onChange={this.onFileUpload} - accept={imageTypes} + accept={imageTypes.join(',')} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx index fecf846ed6c9a..8625487282880 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook.tsx @@ -473,8 +473,6 @@ const WebhookParamsFields: React.FunctionComponent 0 && body !== undefined} mode="json" width="100%" height="200px" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx index a2ef67be7bca2..866a7e497742c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx @@ -17,7 +17,7 @@ import { EuiSelect, EuiSpacer, EuiComboBox, - EuiComboBoxOptionProps, + EuiComboBoxOptionOption, EuiFormRow, EuiCallOut, } from '@elastic/eui'; @@ -104,7 +104,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent>([]); - const [indexOptions, setIndexOptions] = useState([]); + const [indexOptions, setIndexOptions] = useState([]); const [timeFieldOptions, setTimeFieldOptions] = useState([firstFieldOption]); const [isIndiciesLoading, setIsIndiciesLoading] = useState(false); @@ -256,7 +256,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent { + onChange={async (selected: EuiComboBoxOptionOption[]) => { setAlertParams( 'index', selected.map(aSelected => aSelected.value) diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.test.tsx index 2e674f4fb47b1..4d0017ce5c8e6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.test.tsx @@ -23,6 +23,7 @@ describe('of expression', () => { expect(wrapper.find('[data-test-subj="availablefieldsOptionsComboBox"]')) .toMatchInlineSnapshot(` { ); expect(wrapper.find('[data-test-subj="availablefieldsOptionsComboBox"]')) .toMatchInlineSnapshot(` - + /> `); }); diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx index c906d05be64be..b9fce52b480ef 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx @@ -374,7 +374,6 @@ export const JsonWatchEditSimulate = ({ errors={executeWatchErrors} > = ({ errors={errors} > { value: anIndex, }; })} - onChange={async (selected: EuiComboBoxOptionProps[]) => { + onChange={async (selected: EuiComboBoxOptionOption[]) => { setWatchProperty( 'index', selected.map(aSelected => aSelected.value) diff --git a/x-pack/typings/@elastic/eui/index.d.ts b/x-pack/typings/@elastic/eui/index.d.ts index 688d1a2fa127d..ea7a81fa986ce 100644 --- a/x-pack/typings/@elastic/eui/index.d.ts +++ b/x-pack/typings/@elastic/eui/index.d.ts @@ -7,7 +7,6 @@ // TODO: Remove once typescript definitions are in EUI declare module '@elastic/eui' { - export const EuiCodeEditor: React.FC; export const Query: any; } diff --git a/yarn.lock b/yarn.lock index dde08490d62f0..1cf77d50d7dbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1952,16 +1952,17 @@ tabbable "^1.1.0" uuid "^3.1.0" -"@elastic/eui@19.0.0": - version "19.0.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-19.0.0.tgz#cf7d644945c95997d442585cf614e853f173746e" - integrity sha512-8/USz56MYhu6bV4oecJct7tsdi0ktErOIFLobNmQIKdxDOni/KpttX6IHqxM7OuIWi1AEMXoIozw68+oyL/uKQ== +"@elastic/eui@20.0.2": + version "20.0.2" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-20.0.2.tgz#c64b16fef15da6aa9e627d45cdd372f1fc676359" + integrity sha512-8TtazI7RO1zJH4Qkl6TZKvAxaFG9F8BEdwyGmbGhyvXOJbkvttRzoaEg9jSQpKr+z7w2vsjGNbza/fEAE41HOA== dependencies: "@types/chroma-js" "^1.4.3" "@types/enzyme" "^3.1.13" "@types/lodash" "^4.14.116" "@types/numeral" "^0.0.25" "@types/react-beautiful-dnd" "^10.1.0" + "@types/react-input-autosize" "^2.0.2" "@types/react-virtualized" "^9.18.7" chroma-js "^2.0.4" classnames "^2.2.5" @@ -5011,6 +5012,13 @@ dependencies: "@types/react" "*" +"@types/react-input-autosize@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/react-input-autosize/-/react-input-autosize-2.0.2.tgz#6ccdfb100c21b6096c1a04c3c3fac196b0ce61c1" + integrity sha512-QzewaD5kog7c6w5e3dretb+50oM8RDdDvVumQKCtPjI6VHyR8lA/HxCiTrv5l9Vgbi4NCitYuix/NorOevlrng== + dependencies: + "@types/react" "*" + "@types/react-intl@^2.3.15": version "2.3.17" resolved "https://registry.yarnpkg.com/@types/react-intl/-/react-intl-2.3.17.tgz#e1fc6e46e8af58bdef9531259d509380a8a99e8e" From 2f97b4c06aefb01c0d800b4b695710547db60592 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Thu, 5 Mar 2020 11:32:32 -0800 Subject: [PATCH 04/12] [DOCS] Updates Snapshot and Restore doc (#59451) * [DOCS] Updates Snapshot and Restore doc * [DOCS] Incorporates review comment --- .../images/snapshot_permissions.png | Bin 0 -> 86563 bytes .../snapshot-restore/index.asciidoc | 122 ++++++++++-------- 2 files changed, 67 insertions(+), 55 deletions(-) create mode 100644 docs/management/snapshot-restore/images/snapshot_permissions.png diff --git a/docs/management/snapshot-restore/images/snapshot_permissions.png b/docs/management/snapshot-restore/images/snapshot_permissions.png new file mode 100644 index 0000000000000000000000000000000000000000..463d4d6e389c61557cbc90a5231a0581af857586 GIT binary patch literal 86563 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:aD#DJlBNY}#yS6LzfhUC&+RFM$Pip-9+^9ri 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 Elasticsearch*. +You’ll find *Snapshot and Restore* under *Management > Elasticsearch*. With this UI, you can: * Register a repository for storing your snapshots @@ -20,29 +20,42 @@ With this UI, you can: [role="screenshot"] image:management/snapshot-restore/images/snapshot_list.png["Snapshot list"] -Before using this feature, you should be familiar with how snapshots work. -{ref}/snapshot-restore.html[Snapshot and Restore] is a good source for +Before using this feature, you should be familiar with how snapshots work. +{ref}/snapshot-restore.html[Snapshot and Restore] is a good source for more detailed information. +[float] +[[snapshot-permissions]] +=== Required permissions +The minimum required permissions to access *Snapshot and Restore* include: + +* Cluster privileges: `monitor`, `manage_slm`, `cluster:admin/snapshot`, and `cluster:admin/repository` +* Index privileges: `all` on the `monitor` index if you want to access content in the *Restore Status* tab + +You can add these privileges in *Management > Security > Roles*. + +[role="screenshot"] +image:management/snapshot-restore/images/snapshot_permissions.png["Edit Role"] + [float] [[kib-snapshot-register-repository]] === Register a repository -A repository is where your snapshots live. You must register a snapshot -repository before you can perform snapshot and restore operations. +A repository is where your snapshots live. You must register a snapshot +repository before you can perform snapshot and restore operations. -If you don't have a repository, Kibana walks you through the process of -registering one. +If you don't have a repository, Kibana walks you through the process of +registering one. {kib} supports three repository types -out of the box: shared file system, read-only URL, and source-only. -For more information on these repositories and their settings, +out of the box: shared file system, read-only URL, and source-only. +For more information on these repositories and their settings, see {ref}/snapshots-register-repository.html[Repositories]. -To use other repositories, such as S3, see +To use other repositories, such as S3, see {ref}/snapshots-register-repository.html#snapshots-repository-plugins[Repository plugins]. -Once you create a repository, it is listed in the *Repositories* -view. -Click a repository name to view its type, number of snapshots, and settings, +Once you create a repository, it is listed in the *Repositories* +view. +Click a repository name to view its type, number of snapshots, and settings, and to verify status. [role="screenshot"] @@ -53,46 +66,46 @@ image:management/snapshot-restore/images/repository_list.png["Repository list"] [[kib-view-snapshot]] === View your snapshots -A snapshot is a backup taken from a running {es} cluster. You'll find an overview of -your snapshots in the *Snapshots* view, and you can drill down +A snapshot is a backup taken from a running {es} cluster. You'll find an overview of +your snapshots in the *Snapshots* view, and you can drill down into each snapshot for further investigation. [role="screenshot"] image:management/snapshot-restore/images/snapshot_details.png["Snapshot details"] -If you don’t have any snapshots, you can create them from the {kib} <>. The +If you don’t have any snapshots, you can create them from the {kib} <>. The {ref}/snapshots-take-snapshot.html[snapshot API] -takes the current state and data in your index or cluster, and then saves it to a -shared repository. +takes the current state and data in your index or cluster, and then saves it to a +shared repository. -The snapshot process is "smart." Your first snapshot is a complete copy of +The snapshot process is "smart." Your first snapshot is a complete copy of the data in your index or cluster. -All subsequent snapshots save the changes between the existing snapshots and +All subsequent snapshots save the changes between the existing snapshots and the new data. [float] [[kib-restore-snapshot]] === Restore a snapshot -The information stored in a snapshot is not tied to a specific +The information stored in a snapshot is not tied to a specific cluster or a cluster name. This enables you to -restore a snapshot made from one cluster to another cluster. You might +restore a snapshot made from one cluster to another cluster. You might use the restore operation to: * Recover data lost due to a failure * Migrate a current Elasticsearch cluster to a new version * Move data from one cluster to another cluster -To get started, go to the *Snapshots* view, find the -snapshot, and click the restore icon in the *Actions* column. +To get started, go to the *Snapshots* view, find the +snapshot, and click the restore icon in the *Actions* column. The Restore wizard presents -options for the restore operation, including which +options for the restore operation, including which indices to restore and whether to modify the index settings. -You can restore an existing index only if it’s closed and has the same +You can restore an existing index only if it’s closed and has the same number of shards as the index in the snapshot. Once you initiate the restore, you're navigated to the *Restore Status* view, -where you can track the current state for each shard in the snapshot. +where you can track the current state for each shard in the snapshot. [role="screenshot"] image:management/snapshot-restore/images/snapshot-restore.png["Snapshot details"] @@ -102,26 +115,26 @@ image:management/snapshot-restore/images/snapshot-restore.png["Snapshot details" [[kib-snapshot-policy]] === Create a snapshot lifecycle policy -Use a {ref}/snapshot-lifecycle-management-api.html[snapshot lifecycle policy] -to automate the creation and deletion +Use a {ref}/snapshot-lifecycle-management-api.html[snapshot lifecycle policy] +to automate the creation and deletion of cluster snapshots. Taking automatic snapshots: * Ensures your {es} indices and clusters are backed up on a regular basis -* Ensures a recent and relevant snapshot is available if a situation +* Ensures a recent and relevant snapshot is available if a situation arises where a cluster needs to be recovered -* Allows you to manage your snapshots in {kib}, instead of using a +* Allows you to manage your snapshots in {kib}, instead of using a third-party tool - -If you don’t have any snapshot policies, follow the -*Create policy* wizard. It walks you through defining -when and where to take snapshots, the settings you want, + +If you don’t have any snapshot policies, follow the +*Create policy* wizard. It walks you through defining +when and where to take snapshots, the settings you want, and how long to retain snapshots. [role="screenshot"] image:management/snapshot-restore/images/snapshot-retention.png["Snapshot details"] An overview of your policies is on the *Policies* view. -You can drill down into each policy to examine its settings and last successful and failed run. +You can drill down into each policy to examine its settings and last successful and failed run. You can perform the following actions on a snapshot policy: @@ -139,8 +152,8 @@ image:management/snapshot-restore/images/create-policy.png["Snapshot details"] === Delete a snapshot Delete snapshots to manage your repository storage space. -Find the snapshot in the *Snapshots* view and click the trash icon in the -*Actions* column. To delete snapshots in bulk, select their checkboxes, +Find the snapshot in the *Snapshots* view and click the trash icon in the +*Actions* column. To delete snapshots in bulk, select their checkboxes, and then click *Delete snapshots*. [[snapshot-repositories-example]] @@ -159,10 +172,10 @@ Ready to try *Snapshot and Restore*? In this tutorial, you'll learn to: ==== Before you begin -This example shows you how to register a shared file system repository +This example shows you how to register a shared file system repository and store snapshots. -Before you begin, you must register the location of the repository in the -{ref}/snapshots-register-repository.html#snapshots-filesystem-repository[path.repo] setting on +Before you begin, you must register the location of the repository in the +{ref}/snapshots-register-repository.html#snapshots-filesystem-repository[path.repo] setting on your master and data nodes. You can do this in one of two ways: * Edit your `elasticsearch.yml` to include the `path.repo` setting. @@ -175,14 +188,14 @@ your master and data nodes. You can do this in one of two ways: [[register-repo-example]] ==== Register a repository -Use *Snapshot and Restore* to register the repository where your snapshots -will live. +Use *Snapshot and Restore* to register the repository where your snapshots +will live. . Go to *Management > Elasticsearch > Snapshot and Restore*. . Click *Register a repository* in either the introductory message or *Repository view*. . Enter a name for your repository, for example, `my_backup`. . Select *Shared file system*. -+ ++ [role="screenshot"] image:management/snapshot-restore/images/register_repo.png["Register repository"] @@ -205,13 +218,13 @@ Use the {ref}/snapshots-take-snapshot.html[snapshot API] to create a snapshot. [source,js] PUT /_snapshot/my_backup/2019-04-25_snapshot?wait_for_completion=true + -In this example, the snapshot name is `2019-04-25_snapshot`. You can also +In this example, the snapshot name is `2019-04-25_snapshot`. You can also use {ref}/date-math-index-names.html[date math expression] for the snapshot name. + [role="screenshot"] image:management/snapshot-restore/images/create_snapshot.png["Create snapshot"] -. Return to *Snapshot and Restore*. +. Return to *Snapshot and Restore*. + Your new snapshot is available in the *Snapshots* view. @@ -223,7 +236,7 @@ using the repository created in the previous example. . Open the *Policies* view. . Click *Create a policy*. -+ ++ [role="screenshot"] image:management/snapshot-restore/images/create-policy-example.png["Create policy wizard"] @@ -288,17 +301,16 @@ Finally, you'll restore indices from an existing snapshot. |*Index settings* | |Modify index settings -|Toggle to overwrite index settings when they are restored, +|Toggle to overwrite index settings when they are restored, or leave in place to keep existing settings. |Reset index settings -|Toggle to reset index settings back to the default when they are restored, +|Toggle to reset index settings back to the default when they are restored, or leave in place to keep existing settings. |=== . Review your restore settings, and then click *Restore snapshot*. + -The operation loads for a few seconds, -and then you’re navigated to *Restore Status*, +The operation loads for a few seconds, +and then you’re navigated to *Restore Status*, where you can monitor the status of your restored indices. - From 26aed8dc30ab83f1656fe7a6ec354b68a2f1aeb3 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 5 Mar 2020 11:55:30 -0800 Subject: [PATCH 05/12] Extended AlertContextValue with metadata optional property (#59391) * Extended AlertContextValue with metadata optional property * Made metadata generic --- .../threshold/expression.tsx | 3 +- .../application/context/alerts_context.tsx | 3 +- .../sections/alert_form/alert_add.test.tsx | 33 +++++++++++++++---- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx index 866a7e497742c..9a01a7f50c3df 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx @@ -143,7 +143,8 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent 0) { + + if (index && index.length > 0) { const currentEsFields = await getFields(index); const timeFields = getTimeFieldOptions(currentEsFields as any); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx index 1ffebed2eb002..a8578acc24636 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/context/alerts_context.tsx @@ -11,7 +11,7 @@ import { DataPublicPluginSetup } from 'src/plugins/data/public'; import { TypeRegistry } from '../type_registry'; import { AlertTypeModel, ActionTypeModel } from '../../types'; -export interface AlertsContextValue { +export interface AlertsContextValue> { reloadAlerts?: () => Promise; http: HttpSetup; alertTypeRegistry: TypeRegistry; @@ -23,6 +23,7 @@ export interface AlertsContextValue { >; charts?: ChartsPluginSetup; dataFieldsFormats?: DataPublicPluginSetup['fieldFormats']; + metadata?: MetaData; } const AlertsContext = createContext(null as any); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 7bc44eafe7543..1177b41788bd6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -6,11 +6,13 @@ import * as React from 'react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { act } from 'react-dom/test-utils'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFormLabel } from '@elastic/eui'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { AlertAdd } from './alert_add'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult } from '../../../types'; -import { AlertsContextProvider } from '../../context/alerts_context'; +import { AlertsContextProvider, useAlertsContext } from '../../context/alerts_context'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; @@ -18,6 +20,21 @@ import { ReactWrapper } from 'enzyme'; const actionTypeRegistry = actionTypeRegistryMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); +export const TestExpression: React.FunctionComponent = () => { + const alertsContext = useAlertsContext(); + const { metadata } = alertsContext; + + return ( + + + + ); +}; + describe('alert_add', () => { let deps: any; let wrapper: ReactWrapper; @@ -41,7 +58,7 @@ describe('alert_add', () => { validate: (): ValidationResult => { return { errors: {} }; }, - alertParamsExpression: () => , + alertParamsExpression: TestExpression, }; const actionTypeModel = { @@ -77,13 +94,10 @@ describe('alert_add', () => { alertTypeRegistry: deps.alertTypeRegistry, toastNotifications: deps.toastNotifications, uiSettings: deps.uiSettings, + metadata: { test: 'some value', fields: ['test'] }, }} > - {}} - /> + {}} /> ); // Wait for active space to resolve before requesting the component to update @@ -97,5 +111,10 @@ describe('alert_add', () => { await setup(); expect(wrapper.find('[data-test-subj="addAlertFlyoutTitle"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="saveAlertButton"]').exists()).toBeTruthy(); + wrapper + .find('[data-test-subj="my-alert-type-SelectOption"]') + .first() + .simulate('click'); + expect(wrapper.contains('Metadata: some value. Fields: test.')).toBeTruthy(); }); }); From 4bc9e8b4a891c2949e8da9a6a5175144e8753015 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 5 Mar 2020 13:01:42 -0700 Subject: [PATCH 06/12] Fix visual baseline job (#59348) * Establish Percy baselines * move Jenkinsfile changed back to `.ci` directory * rename xpack workers Co-authored-by: Elastic Machine --- .ci/Jenkinsfile_visual_baseline | 14 +-- test/functional/services/elastic_chart.ts | 6 +- test/scripts/jenkins_visual_regression.sh | 14 ++- .../jenkins_xpack_visual_regression.sh | 18 +++- .../services/visual_testing/visual_testing.ts | 7 ++ ...isualization.js => chart_visualization.ts} | 91 +++++++------------ .../tests/discover/{index.js => index.ts} | 8 +- .../{config.js => config.ts} | 11 +-- .../ftr_provider_context.d.ts | 12 +++ x-pack/test/visual_regression/page_objects.ts | 9 ++ x-pack/test/visual_regression/services.ts | 13 +++ .../tests/{login_page.js => login_page.ts} | 4 +- 12 files changed, 119 insertions(+), 88 deletions(-) rename test/visual_regression/tests/discover/{chart_visualization.js => chart_visualization.ts} (55%) rename test/visual_regression/tests/discover/{index.js => index.ts} (86%) rename x-pack/test/visual_regression/{config.js => config.ts} (69%) create mode 100644 x-pack/test/visual_regression/ftr_provider_context.d.ts create mode 100644 x-pack/test/visual_regression/page_objects.ts create mode 100644 x-pack/test/visual_regression/services.ts rename x-pack/test/visual_regression/tests/{login_page.js => login_page.ts} (91%) diff --git a/.ci/Jenkinsfile_visual_baseline b/.ci/Jenkinsfile_visual_baseline index 4a1e0f7d74e07..5c13ccccd9c6f 100644 --- a/.ci/Jenkinsfile_visual_baseline +++ b/.ci/Jenkinsfile_visual_baseline @@ -6,13 +6,15 @@ kibanaLibrary.load() kibanaPipeline(timeoutMinutes: 120) { catchError { parallel([ - workers.base(name: 'oss-visualRegression', label: 'linux && immutable') { - kibanaPipeline.buildOss() - kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh') + 'oss-visualRegression': { + workers.ci(name: 'oss-visualRegression', label: 'linux && immutable', ramDisk: false) { + kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')(1) + } }, - workers.base(name: 'xpack-visualRegression', label: 'linux && immutable') { - kibanaPipeline.buildXpack() - kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh') + 'xpack-visualRegression': { + workers.ci(name: 'xpack-visualRegression', label: 'linux && immutable', ramDisk: false) { + kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')(1) + } }, ]) } diff --git a/test/functional/services/elastic_chart.ts b/test/functional/services/elastic_chart.ts index 45ad157fc5c02..1c3071ac01587 100644 --- a/test/functional/services/elastic_chart.ts +++ b/test/functional/services/elastic_chart.ts @@ -51,11 +51,11 @@ export function ElasticChartProvider({ getService }: FtrProviderContext) { return Number(renderingCount); } - public async waitForRenderingCount(dataTestSubj: string, previousCount = 1) { - await retry.waitFor(`rendering count to be equal to [${previousCount + 1}]`, async () => { + public async waitForRenderingCount(dataTestSubj: string, minimumCount: number) { + await retry.waitFor(`rendering count to be equal to [${minimumCount}]`, async () => { const currentRenderingCount = await this.getVisualizationRenderingCount(dataTestSubj); log.debug(`-- currentRenderingCount=${currentRenderingCount}`); - return currentRenderingCount === previousCount + 1; + return currentRenderingCount >= minimumCount; }); } } diff --git a/test/scripts/jenkins_visual_regression.sh b/test/scripts/jenkins_visual_regression.sh index dda966dea98d0..4fdd197147eac 100755 --- a/test/scripts/jenkins_visual_regression.sh +++ b/test/scripts/jenkins_visual_regression.sh @@ -1,10 +1,18 @@ #!/usr/bin/env bash -source test/scripts/jenkins_test_setup_xpack.sh +source src/dev/ci_setup/setup_env.sh source "$KIBANA_DIR/src/dev/ci_setup/setup_percy.sh" -checks-reporter-with-killswitch "Kibana visual regression tests" \ - yarn run percy exec -t 500 \ +echo " -> building and extracting OSS Kibana distributable for use in functional tests" +node scripts/build --debug --oss +linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" +installDir="$PARENT_DIR/install/kibana" +mkdir -p "$installDir" +tar -xzf "$linuxBuild" -C "$installDir" --strip=1 + +echo " -> running visual regression tests from kibana directory" +checks-reporter-with-killswitch "X-Pack visual regression tests" \ + yarn percy exec -t 500 \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$installDir" \ diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_visual_regression.sh index 6e3d4dd7c249b..73e92da3bad63 100755 --- a/test/scripts/jenkins_xpack_visual_regression.sh +++ b/test/scripts/jenkins_xpack_visual_regression.sh @@ -1,11 +1,21 @@ #!/usr/bin/env bash -source test/scripts/jenkins_test_setup_xpack.sh +source src/dev/ci_setup/setup_env.sh source "$KIBANA_DIR/src/dev/ci_setup/setup_percy.sh" +echo " -> building and extracting default Kibana distributable" +cd "$KIBANA_DIR" +node scripts/build --debug --no-oss +linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" +installDir="$PARENT_DIR/install/kibana" +mkdir -p "$installDir" +tar -xzf "$linuxBuild" -C "$installDir" --strip=1 + +echo " -> running visual regression tests from x-pack directory" +cd "$XPACK_DIR" checks-reporter-with-killswitch "X-Pack visual regression tests" \ - yarn run percy exec -t 500 \ + yarn percy exec -t 500 \ node scripts/functional_tests \ --debug --bail \ - --kibana-install-dir "$KIBANA_INSTALL_DIR" \ - --config test/visual_regression/config.js; + --kibana-install-dir "$installDir" \ + --config test/visual_regression/config.ts; diff --git a/test/visual_regression/services/visual_testing/visual_testing.ts b/test/visual_regression/services/visual_testing/visual_testing.ts index 4ad97f8d98717..0882beecf7f5c 100644 --- a/test/visual_regression/services/visual_testing/visual_testing.ts +++ b/test/visual_regression/services/visual_testing/visual_testing.ts @@ -71,6 +71,13 @@ export async function VisualTestingProvider({ getService }: FtrProviderContext) return new (class VisualTesting { public async snapshot(options: SnapshotOptions = {}) { + if (process.env.DISABLE_VISUAL_TESTING) { + log.warning( + 'Capturing of percy snapshots disabled, would normally capture a snapshot here!' + ); + return; + } + log.debug('Capturing percy snapshot'); if (!currentTest) { diff --git a/test/visual_regression/tests/discover/chart_visualization.js b/test/visual_regression/tests/discover/chart_visualization.ts similarity index 55% rename from test/visual_regression/tests/discover/chart_visualization.js rename to test/visual_regression/tests/discover/chart_visualization.ts index 10ac559b9f982..49c3057a27cb0 100644 --- a/test/visual_regression/tests/discover/chart_visualization.js +++ b/test/visual_regression/tests/discover/chart_visualization.ts @@ -19,8 +19,9 @@ import expect from '@kbn/expect'; -export default function({ getService, getPageObjects }) { - const log = getService('log'); +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const esArchiver = getService('esArchiver'); const browser = getService('browser'); @@ -34,58 +35,56 @@ export default function({ getService, getPageObjects }) { describe('discover', function describeIndexTests() { before(async function() { - log.debug('load kibana index with default index pattern'); await esArchiver.load('discover'); // and load a set of makelogs data await esArchiver.loadIfNeeded('logstash_functional'); await kibanaServer.uiSettings.replace(defaultSettings); - log.debug('discover'); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setDefaultAbsoluteRange(); }); + after(function unloadMakelogs() { + return esArchiver.unload('logstash_functional'); + }); + + async function refreshDiscover() { + await browser.refresh(); + await PageObjects.header.awaitKibanaChrome(); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.discover.waitForChartLoadingComplete(1); + } + + async function takeSnapshot() { + await refreshDiscover(); + await visualTesting.snapshot({ + show: ['discoverChart'], + }); + } + describe('query', function() { this.tags(['skipFirefox']); - let renderCounter = 0; it('should show bars in the correct time zone', async function() { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); - await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); - await visualTesting.snapshot({ - show: ['discoverChart'], - }); + await takeSnapshot(); }); it('should show correct data for chart interval Hourly', async function() { - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Hourly'); - await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); - await visualTesting.snapshot({ - show: ['discoverChart'], - }); + await takeSnapshot(); }); it('should show correct data for chart interval Daily', async function() { - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Daily'); - await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); - await visualTesting.snapshot({ - show: ['discoverChart'], - }); + await takeSnapshot(); }); it('should show correct data for chart interval Weekly', async function() { - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Weekly'); - await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); - await visualTesting.snapshot({ - show: ['discoverChart'], - }); + await takeSnapshot(); }); it('browser back button should show previous interval Daily', async function() { @@ -94,57 +93,31 @@ export default function({ getService, getPageObjects }) { const actualInterval = await PageObjects.discover.getChartInterval(); expect(actualInterval).to.be('Daily'); }); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - await PageObjects.discover.waitUntilSearchingHasFinished(); - await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); - await visualTesting.snapshot({ - show: ['discoverChart'], - }); + await takeSnapshot(); }); it('should show correct data for chart interval Monthly', async function() { - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Monthly'); - await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); - await visualTesting.snapshot({ - show: ['discoverChart'], - }); + await takeSnapshot(); }); it('should show correct data for chart interval Yearly', async function() { - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Yearly'); - await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); - await visualTesting.snapshot({ - show: ['discoverChart'], - }); + await takeSnapshot(); }); it('should show correct data for chart interval Auto', async function() { - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Auto'); - await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); - await visualTesting.snapshot({ - show: ['discoverChart'], - }); + await takeSnapshot(); }); }); describe('time zone switch', () => { it('should show bars in the correct time zone after switching', async function() { await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'America/Phoenix' }); - await browser.refresh(); - await PageObjects.header.awaitKibanaChrome(); + await refreshDiscover(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - await PageObjects.discover.waitUntilSearchingHasFinished(); - await PageObjects.discover.waitForChartLoadingComplete(1); - await visualTesting.snapshot({ - show: ['discoverChart'], - }); + await takeSnapshot(); }); }); }); diff --git a/test/visual_regression/tests/discover/index.js b/test/visual_regression/tests/discover/index.ts similarity index 86% rename from test/visual_regression/tests/discover/index.js rename to test/visual_regression/tests/discover/index.ts index f98aac52aa4cb..d036327ae7475 100644 --- a/test/visual_regression/tests/discover/index.js +++ b/test/visual_regression/tests/discover/index.ts @@ -18,12 +18,12 @@ */ import { DEFAULT_OPTIONS } from '../../services/visual_testing/visual_testing'; +import { FtrProviderContext } from '../../ftr_provider_context'; // Width must be the same as visual_testing or canvas image widths will get skewed const [SCREEN_WIDTH] = DEFAULT_OPTIONS.widths || []; -export default function({ getService, loadTestFile }) { - const esArchiver = getService('esArchiver'); +export default function({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); describe('discover app', function() { @@ -33,10 +33,6 @@ export default function({ getService, loadTestFile }) { return browser.setWindowSize(SCREEN_WIDTH, 1000); }); - after(function unloadMakelogs() { - return esArchiver.unload('logstash_functional'); - }); - loadTestFile(require.resolve('./chart_visualization')); }); } diff --git a/x-pack/test/visual_regression/config.js b/x-pack/test/visual_regression/config.ts similarity index 69% rename from x-pack/test/visual_regression/config.js rename to x-pack/test/visual_regression/config.ts index aff6aaaf4114a..dce17348f75e6 100644 --- a/x-pack/test/visual_regression/config.js +++ b/x-pack/test/visual_regression/config.ts @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { services as ossVisualRegressionServices } from '../../../test/visual_regression/services'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; -export default async function({ readConfigFile }) { +import { services } from './services'; + +export default async function({ readConfigFile }: FtrConfigProviderContext) { const functionalConfig = await readConfigFile(require.resolve('../functional/config')); return { @@ -19,10 +21,7 @@ export default async function({ readConfigFile }) { require.resolve('./tests/infra'), ], - services: { - ...functionalConfig.get('services'), - visualTesting: ossVisualRegressionServices.visualTesting, - }, + services, junit: { reportName: 'X-Pack Visual Regression Tests', diff --git a/x-pack/test/visual_regression/ftr_provider_context.d.ts b/x-pack/test/visual_regression/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..bb257cdcbfe1b --- /dev/null +++ b/x-pack/test/visual_regression/ftr_provider_context.d.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { pageObjects } from './page_objects'; +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/visual_regression/page_objects.ts b/x-pack/test/visual_regression/page_objects.ts new file mode 100644 index 0000000000000..ea3e49d0ccc5e --- /dev/null +++ b/x-pack/test/visual_regression/page_objects.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { pageObjects } from '../functional/page_objects'; + +export { pageObjects }; diff --git a/x-pack/test/visual_regression/services.ts b/x-pack/test/visual_regression/services.ts new file mode 100644 index 0000000000000..447c16281b838 --- /dev/null +++ b/x-pack/test/visual_regression/services.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { services as ossVisualRegressionServices } from '../../../test/visual_regression/services'; +import { services as functionalServices } from '../functional/services'; + +export const services = { + ...functionalServices, + visualTesting: ossVisualRegressionServices.visualTesting, +}; diff --git a/x-pack/test/visual_regression/tests/login_page.js b/x-pack/test/visual_regression/tests/login_page.ts similarity index 91% rename from x-pack/test/visual_regression/tests/login_page.js rename to x-pack/test/visual_regression/tests/login_page.ts index b290b8f819589..ce90669a6bfe1 100644 --- a/x-pack/test/visual_regression/tests/login_page.js +++ b/x-pack/test/visual_regression/tests/login_page.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export default function({ getService, getPageObjects }) { +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const visualTesting = getService('visualTesting'); const testSubjects = getService('testSubjects'); From 944be8009187228a1709796fb1d52849d17822cf Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Thu, 5 Mar 2020 14:16:37 -0700 Subject: [PATCH 07/12] Rename status_page to statusPage (#59186) --- .github/CODEOWNERS | 1 + src/plugins/status_page/kibana.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5948b9672e6d4..de74a2c42be8b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -132,6 +132,7 @@ /src/legacy/server/logging/ @elastic/kibana-platform /src/legacy/server/saved_objects/ @elastic/kibana-platform /src/legacy/server/status/ @elastic/kibana-platform +/src/plugins/status_page/ @elastic/kibana-platform /src/dev/run_check_core_api_changes.ts @elastic/kibana-platform # Security diff --git a/src/plugins/status_page/kibana.json b/src/plugins/status_page/kibana.json index edebf8cb12239..0d54f6a39e2b1 100644 --- a/src/plugins/status_page/kibana.json +++ b/src/plugins/status_page/kibana.json @@ -1,5 +1,5 @@ { - "id": "status_page", + "id": "statusPage", "version": "kibana", "server": false, "ui": true From d5497d99b2c67c81411d694454d406e8ae361f75 Mon Sep 17 00:00:00 2001 From: marshallmain <55718608+marshallmain@users.noreply.github.com> Date: Thu, 5 Mar 2020 16:35:54 -0500 Subject: [PATCH 08/12] [Endpoint] Fix alert list functional test error (#59357) * fix the functional test error * fix linting Co-authored-by: Elastic Machine --- x-pack/test/functional/apps/endpoint/alert_list.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/test/functional/apps/endpoint/alert_list.ts b/x-pack/test/functional/apps/endpoint/alert_list.ts index 089fa487ef1b8..eae7713c37a06 100644 --- a/x-pack/test/functional/apps/endpoint/alert_list.ts +++ b/x-pack/test/functional/apps/endpoint/alert_list.ts @@ -8,10 +8,12 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const pageObjects = getPageObjects(['common', 'endpoint']); const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); describe('Endpoint Alert List', function() { this.tags(['ciGroup7']); before(async () => { + await esArchiver.load('endpoint/alerts/api_feature'); await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/alerts'); }); @@ -21,5 +23,9 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('includes Alert list data grid', async () => { await testSubjects.existOrFail('alertListGrid'); }); + + after(async () => { + await esArchiver.unload('endpoint/alerts/api_feature'); + }); }); } From 75dabc5dce507074ba60cdec947b04d119dc9e5b Mon Sep 17 00:00:00 2001 From: Alex Holmansky Date: Thu, 5 Mar 2020 17:21:23 -0500 Subject: [PATCH 09/12] Temporarily disabling PR project mappings (#59485) * Use diagnostics-enable action in the workflow. Issue: #56526 * Update workflow to use v1.0.2 of the action * Adding a new test workflow that uses a personal access token * Remove an extra coma * Updated project-assigner action version and access key * Deleted the test workflow * Temporarily commenting out project mappings while we debug the permissions issues Co-authored-by: Elastic Machine --- .github/workflows/pr-project-assigner.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-project-assigner.yml b/.github/workflows/pr-project-assigner.yml index 517aefb36e8d6..9d4bcacb4fe3b 100644 --- a/.github/workflows/pr-project-assigner.yml +++ b/.github/workflows/pr-project-assigner.yml @@ -13,8 +13,8 @@ jobs: with: issue-mappings: | [ - { "label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173897 }, - { "label": "Feature:Lens", "projectName": "Lens", "columnId": 6219362 }, - { "label": "Team:Canvas", "projectName": "canvas", "columnId": 6187580 } +# { "label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173897 }, +# { "label": "Feature:Lens", "projectName": "Lens", "columnId": 6219362 }, +# { "label": "Team:Canvas", "projectName": "canvas", "columnId": 6187580 } ] ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} From c3f8647c3ed0e4ff5fb02546d25924f2a9a378d0 Mon Sep 17 00:00:00 2001 From: Alex Holmansky Date: Thu, 5 Mar 2020 17:23:48 -0500 Subject: [PATCH 10/12] Revert "Temporarily disabling PR project mappings (#59485)" (#59491) This reverts commit 75dabc5dce507074ba60cdec947b04d119dc9e5b. --- .github/workflows/pr-project-assigner.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-project-assigner.yml b/.github/workflows/pr-project-assigner.yml index 9d4bcacb4fe3b..517aefb36e8d6 100644 --- a/.github/workflows/pr-project-assigner.yml +++ b/.github/workflows/pr-project-assigner.yml @@ -13,8 +13,8 @@ jobs: with: issue-mappings: | [ -# { "label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173897 }, -# { "label": "Feature:Lens", "projectName": "Lens", "columnId": 6219362 }, -# { "label": "Team:Canvas", "projectName": "canvas", "columnId": 6187580 } + { "label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173897 }, + { "label": "Feature:Lens", "projectName": "Lens", "columnId": 6219362 }, + { "label": "Team:Canvas", "projectName": "canvas", "columnId": 6187580 } ] ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} From e869695d7349c9edbbf73b2b9c725d9e5a9d8fee Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 5 Mar 2020 14:57:32 -0800 Subject: [PATCH 11/12] Added possibility to embed connectors create and edit flyouts (#58514) * Added possibility to embed connectors flyout * Fixed type checks and removed example from siem start page * Fixed jest tests * Fixed failing tests * fixed type check * Added config for siem tests * Fixed failing tests * Fixed due to comments * Added missing documentation --- x-pack/plugins/triggers_actions_ui/README.md | 217 +++++++++++++++++- .../context/actions_connectors_context.tsx | 18 +- .../action_connector_form.test.tsx | 13 +- .../action_type_menu.test.tsx | 42 ++-- .../action_type_menu.tsx | 44 +++- .../connector_add_flyout.test.tsx | 60 +++-- .../connector_add_flyout.tsx | 61 +++-- .../connector_add_modal.test.tsx | 50 +--- .../connector_edit_flyout.test.tsx | 27 +-- .../connector_edit_flyout.tsx | 48 ++-- .../components/actions_connectors_list.tsx | 20 +- .../sections/alert_form/alert_edit.test.tsx | 7 +- .../sections/alert_form/alert_form.test.tsx | 186 +++++++-------- .../triggers_actions_ui/public/index.ts | 5 + .../triggers_actions_ui/public/plugin.ts | 73 +++--- .../apps/triggers_actions_ui/alerts.ts | 3 + 16 files changed, 539 insertions(+), 335 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index c6a7808356b86..ccd33c99f9e1c 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -43,6 +43,8 @@ Table of Contents - [Action type model definition](#action-type-model-definition) - [Register action type model](#register-action-type-model) - [Create and register new action type UI example](#reate-and-register-new-action-type-ui-example) + - [Embed the Create Connector flyout within any Kibana plugin](#embed-the-create-connector-flyout-within-any-kibana-plugin) + - [Embed the Edit Connector flyout within any Kibana plugin](#embed-the-edit-connector-flyout-within-any-kibana-plugin) ## Built-in Alert Types @@ -667,6 +669,7 @@ const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); uiSettings, charts, dataFieldsFormats, + metadata: { test: 'some value', fields: ['test'] }, }} > @@ -690,7 +693,7 @@ interface AlertAddProps { AlertsContextProvider value options: ``` -export interface AlertsContextValue { +export interface AlertsContextValue> { addFlyoutVisible: boolean; setAddFlyoutVisibility: React.Dispatch>; reloadAlerts?: () => Promise; @@ -704,6 +707,7 @@ export interface AlertsContextValue { >; charts?: ChartsPluginSetup; dataFieldsFormats?: Pick; + metadata?: MetaData; } ``` @@ -719,6 +723,7 @@ export interface AlertsContextValue { |toastNotifications|Optional toast messages.| |charts|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.| |dataFieldsFormats|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.| +|metadata|Optional generic property, which allows to define component specific metadata. This metadata can be used for passing down preloaded data for Alert type expression component.| ## Build and register Action Types @@ -1198,3 +1203,213 @@ Clicking on the select card for `Example Action Type` will open the action type or create a new connector: ![Example Action Type with empty connectors list](https://i.imgur.com/EamA9Xv.png) + +## Embed the Create Connector flyout within any Kibana plugin + +Follow the instructions bellow to embed the Create Connector flyout within any Kibana plugin: +1. Add TriggersAndActionsUIPublicPluginSetup and TriggersAndActionsUIPublicPluginStart to Kibana plugin setup dependencies: + +``` +import { + TriggersAndActionsUIPublicPluginSetup, + TriggersAndActionsUIPublicPluginStart, + } from '../../../../../x-pack/plugins/triggers_actions_ui/public'; + +triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; +... + +triggers_actions_ui: TriggersAndActionsUIPublicPluginStart; +``` +Then this dependency will be used to embed Create Connector flyout or register new action type. + +2. Add Create Connector flyout to React component: +``` +// import section +import { ActionsConnectorsContextProvider, ConnectorAddFlyout } from '../../../../../../../triggers_actions_ui/public'; + +// in the component state definition section +const [addFlyoutVisible, setAddFlyoutVisibility] = useState(false); + +// load required dependancied +const { http, triggers_actions_ui, toastNotifications, capabilities } = useKibana().services; + +const connector = { + secrets: {}, + id: 'test', + actionTypeId: '.index', + actionType: 'Index', + name: 'action-connector', + referencedByCount: 0, + config: {}, + }; + +// UI control item for open flyout + setAddFlyoutVisibility(true)} +> + + + +// in render section of component + + + +``` + +ConnectorAddFlyout Props definition: +``` +export interface ConnectorAddFlyoutProps { + addFlyoutVisible: boolean; + setAddFlyoutVisibility: React.Dispatch>; + actionTypes?: ActionType[]; +} +``` + +|Property|Description| +|---|---| +|addFlyoutVisible|Visibility state of the Create Connector flyout.| +|setAddFlyoutVisibility|Function for changing visibility state of the Create Connector flyout.| +|actionTypes|Optional property, that allows to define only specific action types list which is available for a current plugin.| + +ActionsConnectorsContextValue options: +``` +export interface ActionsConnectorsContextValue { + http: HttpSetup; + actionTypeRegistry: TypeRegistry; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + capabilities: ApplicationStart['capabilities']; + reloadConnectors?: () => Promise; +} +``` + +|Property|Description| +|---|---| +|http|HttpSetup needed for executing API calls.| +|actionTypeRegistry|Registry for action types.| +|capabilities|Property, which is defining action current user usage capabilities like canSave or canDelete.| +|toastNotifications|Toast messages.| +|reloadConnectors|Optional function, which will be executed if connector was saved sucsessfuly, like reload list of connecotrs.| + + +## Embed the Edit Connector flyout within any Kibana plugin + +Follow the instructions bellow to embed the Edit Connector flyout within any Kibana plugin: +1. Add TriggersAndActionsUIPublicPluginSetup and TriggersAndActionsUIPublicPluginStart to Kibana plugin setup dependencies: + +``` +import { + TriggersAndActionsUIPublicPluginSetup, + TriggersAndActionsUIPublicPluginStart, + } from '../../../../../x-pack/plugins/triggers_actions_ui/public'; + +triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; +... + +triggers_actions_ui: TriggersAndActionsUIPublicPluginStart; +``` +Then this dependency will be used to embed Edit Connector flyout. + +2. Add Create Connector flyout to React component: +``` +// import section +import { ActionsConnectorsContextProvider, ConnectorEditFlyout } from '../../../../../../../triggers_actions_ui/public'; + +// in the component state definition section +const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); + +// load required dependancied +const { http, triggers_actions_ui, toastNotifications, capabilities } = useKibana().services; + +// UI control item for open flyout + setEditFlyoutVisibility(true)} +> + + + +// in render section of component + + + + +``` + +ConnectorEditFlyout Props definition: +``` +export interface ConnectorEditProps { + initialConnector: ActionConnectorTableItem; + editFlyoutVisible: boolean; + setEditFlyoutVisibility: React.Dispatch>; +} +``` + +|Property|Description| +|---|---| +|initialConnector|Property, that allows to define the initial state of edited connector.| +|editFlyoutVisible|Visibility state of the Edit Connector flyout.| +|setEditFlyoutVisibility|Function for changing visibility state of the Edit Connector flyout.| + +ActionsConnectorsContextValue options: +``` +export interface ActionsConnectorsContextValue { + http: HttpSetup; + actionTypeRegistry: TypeRegistry; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + capabilities: ApplicationStart['capabilities']; + reloadConnectors?: () => Promise; +} +``` + +|Property|Description| +|---|---| +|http|HttpSetup needed for executing API calls.| +|actionTypeRegistry|Registry for action types.| +|capabilities|Property, which is defining action current user usage capabilities like canSave or canDelete.| +|toastNotifications|Toast messages.| +|reloadConnectors|Optional function, which will be executed if connector was saved sucsessfuly, like reload list of connecotrs.| diff --git a/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx index 11786950d0f26..b49cdc3d7d8b8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx @@ -5,15 +5,19 @@ */ import React, { createContext, useContext } from 'react'; -import { ActionType } from '../../types'; +import { HttpSetup, ToastsApi, ApplicationStart } from 'kibana/public'; +import { ActionTypeModel } from '../../types'; +import { TypeRegistry } from '../type_registry'; export interface ActionsConnectorsContextValue { - addFlyoutVisible: boolean; - editFlyoutVisible: boolean; - setEditFlyoutVisibility: React.Dispatch>; - setAddFlyoutVisibility: React.Dispatch>; - actionTypesIndex: Record | undefined; - reloadConnectors: () => Promise; + http: HttpSetup; + actionTypeRegistry: TypeRegistry; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + capabilities: ApplicationStart['capabilities']; + reloadConnectors?: () => Promise; } const ActionsConnectorsContext = createContext(null as any); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx index f7becb16c244a..800863e46034e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx @@ -9,26 +9,21 @@ import { coreMock } from '../../../../../../../src/core/public/mocks'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult, ActionConnector } from '../../../types'; import { ActionConnectorForm } from './action_connector_form'; +import { ActionsConnectorsContextValue } from '../../context/actions_connectors_context'; const actionTypeRegistry = actionTypeRegistryMock.create(); describe('action_connector_form', () => { - let deps: any; + let deps: ActionsConnectorsContextValue; beforeAll(async () => { const mocks = coreMock.createSetup(); const [ { - chrome, - docLinks, application: { capabilities }, }, ] = await mocks.getStartServices(); deps = { - chrome, - docLinks, toastNotifications: mocks.notifications.toasts, - injectedMetadata: mocks.injectedMetadata, http: mocks.http, - uiSettings: mocks.uiSettings, capabilities: { ...capabilities, actions: { @@ -37,11 +32,7 @@ describe('action_connector_form', () => { show: true, }, }, - legacy: { - MANAGEMENT_BREADCRUMB: { set: () => {} } as any, - }, actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: {} as any, }; }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx index c1c6d9d94e810..4f098165033e7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx @@ -6,31 +6,28 @@ import * as React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; +import { + ActionsConnectorsContextProvider, + ActionsConnectorsContextValue, +} from '../../context/actions_connectors_context'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ActionTypeMenu } from './action_type_menu'; import { ValidationResult } from '../../../types'; const actionTypeRegistry = actionTypeRegistryMock.create(); describe('connector_add_flyout', () => { - let deps: any; + let deps: ActionsConnectorsContextValue; beforeAll(async () => { const mockes = coreMock.createSetup(); const [ { - chrome, - docLinks, application: { capabilities }, }, ] = await mockes.getStartServices(); deps = { - chrome, - docLinks, - toastNotifications: mockes.notifications.toasts, - injectedMetadata: mockes.injectedMetadata, http: mockes.http, - uiSettings: mockes.uiSettings, + toastNotifications: mockes.notifications.toasts, capabilities: { ...capabilities, actions: { @@ -39,11 +36,7 @@ describe('connector_add_flyout', () => { show: true, }, }, - legacy: { - MANAGEMENT_BREADCRUMB: { set: () => {} } as any, - }, actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: {} as any, }; }); @@ -68,14 +61,10 @@ describe('connector_add_flyout', () => { const wrapper = mountWithIntl( {}, - editFlyoutVisible: false, - setEditFlyoutVisibility: state => {}, - actionTypesIndex: { - 'first-action-type': { id: 'first-action-type', name: 'first', enabled: true }, - 'second-action-type': { id: 'second-action-type', name: 'second', enabled: true }, - }, + http: deps!.http, + actionTypeRegistry: deps!.actionTypeRegistry, + capabilities: deps!.capabilities, + toastNotifications: deps!.toastNotifications, reloadConnectors: () => { return new Promise(() => {}); }, @@ -83,12 +72,17 @@ describe('connector_add_flyout', () => { > ); - expect(wrapper.find('[data-test-subj="first-action-type-card"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="second-action-type-card"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="my-action-type-card"]').exists()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx index ddd08cf6d6d79..a63665a68fb6b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx @@ -3,24 +3,46 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiFlexItem, EuiCard, EuiIcon, EuiFlexGrid } from '@elastic/eui'; -import { ActionType, ActionTypeModel } from '../../../types'; +import { i18n } from '@kbn/i18n'; +import { ActionType, ActionTypeIndex } from '../../../types'; +import { loadActionTypes } from '../../lib/action_connector_api'; import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; -import { TypeRegistry } from '../../type_registry'; interface Props { onActionTypeChange: (actionType: ActionType) => void; - actionTypeRegistry: TypeRegistry; + actionTypes?: ActionType[]; } -export const ActionTypeMenu = ({ onActionTypeChange, actionTypeRegistry }: Props) => { - const { actionTypesIndex } = useActionsConnectorsContext(); - if (!actionTypesIndex) { - return null; - } +export const ActionTypeMenu = ({ onActionTypeChange, actionTypes }: Props) => { + const { http, toastNotifications, actionTypeRegistry } = useActionsConnectorsContext(); + const [actionTypesIndex, setActionTypesIndex] = useState(undefined); - const actionTypes = Object.entries(actionTypesIndex) + useEffect(() => { + (async () => { + try { + const availableActionTypes = actionTypes ?? (await loadActionTypes({ http })); + const index: ActionTypeIndex = {}; + for (const actionTypeItem of availableActionTypes) { + index[actionTypeItem.id] = actionTypeItem; + } + setActionTypesIndex(index); + } catch (e) { + if (toastNotifications) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.actionsConnectorsList.unableToLoadActionTypesMessage', + { defaultMessage: 'Unable to load action types' } + ), + }); + } + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const registeredActionTypes = Object.entries(actionTypesIndex ?? []) .filter(([index]) => actionTypeRegistry.has(index)) .map(([index, actionType]) => { const actionTypeModel = actionTypeRegistry.get(index); @@ -33,7 +55,7 @@ export const ActionTypeMenu = ({ onActionTypeChange, actionTypeRegistry }: Props }; }); - const cardNodes = actionTypes + const cardNodes = registeredActionTypes .sort((a, b) => a.name.localeCompare(b.name)) .map((item, index) => { return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx index 6b87002a1d2cf..cf0edbe422495 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx @@ -7,37 +7,28 @@ import * as React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { ConnectorAddFlyout } from './connector_add_flyout'; -import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; +import { + ActionsConnectorsContextProvider, + ActionsConnectorsContextValue, +} from '../../context/actions_connectors_context'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult } from '../../../types'; -import { AppContextProvider } from '../../app_context'; -import { AppDeps } from '../../app'; -import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; const actionTypeRegistry = actionTypeRegistryMock.create(); describe('connector_add_flyout', () => { - let deps: AppDeps | null; + let deps: ActionsConnectorsContextValue; beforeAll(async () => { const mocks = coreMock.createSetup(); const [ { - chrome, - docLinks, application: { capabilities }, }, ] = await mocks.getStartServices(); deps = { - chrome, - docLinks, - dataPlugin: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), toastNotifications: mocks.notifications.toasts, - injectedMetadata: mocks.injectedMetadata, http: mocks.http, - uiSettings: mocks.uiSettings, capabilities: { ...capabilities, actions: { @@ -46,9 +37,7 @@ describe('connector_add_flyout', () => { show: true, }, }, - setBreadcrumbs: jest.fn(), actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: {} as any, }; }); @@ -71,24 +60,29 @@ describe('connector_add_flyout', () => { actionTypeRegistry.has.mockReturnValue(true); const wrapper = mountWithIntl( - - {}, - editFlyoutVisible: false, - setEditFlyoutVisibility: state => {}, - actionTypesIndex: { - 'my-action-type': { id: 'my-action-type', name: 'test', enabled: true }, + { + return new Promise(() => {}); + }, + }} + > + {}} + actionTypes={[ + { + id: actionType.id, + enabled: true, + name: 'Test', }, - reloadConnectors: () => { - return new Promise(() => {}); - }, - }} - > - - - + ]} + /> + ); expect(wrapper.find('ActionTypeMenu')).toHaveLength(1); expect(wrapper.find('[data-test-subj="my-action-type-card"]').exists()).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 1eabf2441da4f..1b86116781084 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -20,18 +20,33 @@ import { EuiBetaBadge, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; import { ActionTypeMenu } from './action_type_menu'; import { ActionConnectorForm, validateBaseProperties } from './action_connector_form'; import { ActionType, ActionConnector, IErrorObject } from '../../../types'; -import { useAppDependencies } from '../../app_context'; import { connectorReducer } from './connector_reducer'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { createActionConnector } from '../../lib/action_connector_api'; +import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; + +export interface ConnectorAddFlyoutProps { + addFlyoutVisible: boolean; + setAddFlyoutVisibility: React.Dispatch>; + actionTypes?: ActionType[]; +} -export const ConnectorAddFlyout = () => { +export const ConnectorAddFlyout = ({ + addFlyoutVisible, + setAddFlyoutVisibility, + actionTypes, +}: ConnectorAddFlyoutProps) => { let hasErrors = false; - const { http, toastNotifications, capabilities, actionTypeRegistry } = useAppDependencies(); + const { + http, + toastNotifications, + capabilities, + actionTypeRegistry, + reloadConnectors, + } = useActionsConnectorsContext(); const [actionType, setActionType] = useState(undefined); // hooks @@ -48,11 +63,6 @@ export const ConnectorAddFlyout = () => { dispatch({ command: { type: 'setConnector' }, payload: { key: 'connector', value } }); }; - const { - addFlyoutVisible, - setAddFlyoutVisibility, - reloadConnectors, - } = useActionsConnectorsContext(); const [isSaving, setIsSaving] = useState(false); const closeFlyout = useCallback(() => { @@ -79,10 +89,7 @@ export const ConnectorAddFlyout = () => { let actionTypeModel; if (!actionType) { currentForm = ( - + ); } else { actionTypeModel = actionTypeRegistry.get(actionType.id); @@ -108,17 +115,19 @@ export const ConnectorAddFlyout = () => { const onActionConnectorSave = async (): Promise => await createActionConnector({ http, connector }) .then(savedConnector => { - toastNotifications.addSuccess( - i18n.translate( - 'xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText', - { - defaultMessage: "Created '{connectorName}'", - values: { - connectorName: savedConnector.name, - }, - } - ) - ); + if (toastNotifications) { + toastNotifications.addSuccess( + i18n.translate( + 'xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText', + { + defaultMessage: "Created '{connectorName}'", + values: { + connectorName: savedConnector.name, + }, + } + ) + ); + } return savedConnector; }) .catch(errorRes => { @@ -218,7 +227,9 @@ export const ConnectorAddFlyout = () => { setIsSaving(false); if (savedAction) { closeFlyout(); - reloadConnectors(); + if (reloadConnectors) { + reloadConnectors(); + } } }} > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx index d9f3e98919d76..94c2b823e8bcf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx @@ -7,35 +7,24 @@ import * as React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { ConnectorAddModal } from './connector_add_modal'; -import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult } from '../../../types'; -import { AppDeps } from '../../app'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; -import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; +import { ActionsConnectorsContextValue } from '../../context/actions_connectors_context'; const actionTypeRegistry = actionTypeRegistryMock.create(); describe('connector_add_modal', () => { - let deps: AppDeps | null; + let deps: ActionsConnectorsContextValue; beforeAll(async () => { const mocks = coreMock.createSetup(); const [ { - chrome, - docLinks, application: { capabilities }, }, ] = await mocks.getStartServices(); deps = { - chrome, - docLinks, - dataPlugin: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), toastNotifications: mocks.notifications.toasts, - injectedMetadata: mocks.injectedMetadata, http: mocks.http, - uiSettings: mocks.uiSettings, capabilities: { ...capabilities, actions: { @@ -44,9 +33,7 @@ describe('connector_add_modal', () => { show: true, }, }, - setBreadcrumbs: jest.fn(), actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: {} as any, }; }); it('renders connector modal form if addModalVisible is true', () => { @@ -75,30 +62,15 @@ describe('connector_add_modal', () => { const wrapper = deps ? mountWithIntl( - {}, - editFlyoutVisible: false, - setEditFlyoutVisibility: state => {}, - actionTypesIndex: { - 'my-action-type': { id: 'my-action-type', name: 'test', enabled: true }, - }, - reloadConnectors: () => { - return new Promise(() => {}); - }, - }} - > - {}} - actionType={actionType} - http={deps.http} - actionTypeRegistry={deps.actionTypeRegistry} - alertTypeRegistry={deps.alertTypeRegistry} - toastNotifications={deps.toastNotifications} - /> - + {}} + actionType={actionType} + http={deps.http} + actionTypeRegistry={deps.actionTypeRegistry} + alertTypeRegistry={{} as any} + toastNotifications={deps.toastNotifications} + /> ) : undefined; expect(wrapper?.find('EuiModalHeader')).toHaveLength(1); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx index a82003759d973..f9aa2cad8bfc6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx @@ -11,8 +11,6 @@ import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ValidationResult } from '../../../types'; import { ConnectorEditFlyout } from './connector_edit_flyout'; import { AppContextProvider } from '../../app_context'; -import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; const actionTypeRegistry = actionTypeRegistryMock.create(); let deps: any; @@ -22,18 +20,11 @@ describe('connector_edit_flyout', () => { const mockes = coreMock.createSetup(); const [ { - chrome, - docLinks, application: { capabilities }, }, ] = await mockes.getStartServices(); deps = { - chrome, - docLinks, - dataPlugin: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, - injectedMetadata: mockes.injectedMetadata, http: mockes.http, uiSettings: mockes.uiSettings, capabilities: { @@ -44,7 +35,6 @@ describe('connector_edit_flyout', () => { show: true, }, }, - setBreadcrumbs: jest.fn(), actionTypeRegistry: actionTypeRegistry as any, alertTypeRegistry: {} as any, }; @@ -82,19 +72,20 @@ describe('connector_edit_flyout', () => { {}, - editFlyoutVisible: true, - setEditFlyoutVisibility: state => {}, - actionTypesIndex: { - 'test-action-type-id': { id: 'test-action-type-id', name: 'test', enabled: true }, - }, + http: deps.http, + toastNotifications: deps.toastNotifications, + capabilities: deps.capabilities, + actionTypeRegistry: deps.actionTypeRegistry, reloadConnectors: () => { return new Promise(() => {}); }, }} > - + {}} + /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index 6fe555fd74b39..c52bb8cc08f6f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -19,27 +19,33 @@ import { EuiBetaBadge, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; import { ActionConnectorForm, validateBaseProperties } from './action_connector_form'; -import { useAppDependencies } from '../../app_context'; import { ActionConnectorTableItem, ActionConnector, IErrorObject } from '../../../types'; import { connectorReducer } from './connector_reducer'; import { updateActionConnector } from '../../lib/action_connector_api'; import { hasSaveActionsCapability } from '../../lib/capabilities'; +import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; export interface ConnectorEditProps { initialConnector: ActionConnectorTableItem; + editFlyoutVisible: boolean; + setEditFlyoutVisibility: React.Dispatch>; } -export const ConnectorEditFlyout = ({ initialConnector }: ConnectorEditProps) => { +export const ConnectorEditFlyout = ({ + initialConnector, + editFlyoutVisible, + setEditFlyoutVisibility, +}: ConnectorEditProps) => { let hasErrors = false; - const { http, toastNotifications, capabilities, actionTypeRegistry } = useAppDependencies(); - const canSave = hasSaveActionsCapability(capabilities); const { - editFlyoutVisible, - setEditFlyoutVisibility, + http, + toastNotifications, + capabilities, + actionTypeRegistry, reloadConnectors, } = useActionsConnectorsContext(); + const canSave = hasSaveActionsCapability(capabilities); const closeFlyout = useCallback(() => setEditFlyoutVisibility(false), [setEditFlyoutVisibility]); const [{ connector }, dispatch] = useReducer(connectorReducer, { connector: { ...initialConnector, secrets: {} }, @@ -63,17 +69,19 @@ export const ConnectorEditFlyout = ({ initialConnector }: ConnectorEditProps) => const onActionConnectorSave = async (): Promise => await updateActionConnector({ http, connector, id: connector.id }) .then(savedConnector => { - toastNotifications.addSuccess( - i18n.translate( - 'xpack.triggersActionsUI.sections.editConnectorForm.updateSuccessNotificationText', - { - defaultMessage: "Updated '{connectorName}'", - values: { - connectorName: savedConnector.name, - }, - } - ) - ); + if (toastNotifications) { + toastNotifications.addSuccess( + i18n.translate( + 'xpack.triggersActionsUI.sections.editConnectorForm.updateSuccessNotificationText', + { + defaultMessage: "Updated '{connectorName}'", + values: { + connectorName: savedConnector.name, + }, + } + ) + ); + } return savedConnector; }) .catch(errorRes => { @@ -151,7 +159,9 @@ export const ConnectorEditFlyout = ({ initialConnector }: ConnectorEditProps) => setIsSaving(false); if (savedAction) { closeFlyout(); - reloadConnectors(); + if (reloadConnectors) { + reloadConnectors(); + } } }} > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index f48e27791419d..4e514281be0ea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -18,16 +18,16 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ActionsConnectorsContextProvider } from '../../../context/actions_connectors_context'; import { useAppDependencies } from '../../../app_context'; import { loadAllActions, loadActionTypes } from '../../../lib/action_connector_api'; import { ActionConnector, ActionConnectorTableItem, ActionTypeIndex } from '../../../../types'; import { ConnectorAddFlyout, ConnectorEditFlyout } from '../../action_connector_form'; import { hasDeleteActionsCapability, hasSaveActionsCapability } from '../../../lib/capabilities'; import { DeleteConnectorsModal } from '../../../components/delete_connectors_modal'; +import { ActionsConnectorsContextProvider } from '../../../context/actions_connectors_context'; export const ActionsConnectorsList: React.FunctionComponent = () => { - const { http, toastNotifications, capabilities } = useAppDependencies(); + const { http, toastNotifications, capabilities, actionTypeRegistry } = useAppDependencies(); const canDelete = hasDeleteActionsCapability(capabilities); const canSave = hasSaveActionsCapability(capabilities); @@ -377,19 +377,23 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { {data.length === 0 && !canSave && noPermissionPrompt} - + {editedConnectorItem ? ( ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx index d216b4d2a4afe..4ebeba3924faf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx @@ -20,7 +20,7 @@ describe('alert_edit', () => { let deps: any; let wrapper: ReactWrapper; - beforeAll(async () => { + async function setup() { const mockes = coreMock.createSetup(); deps = { toastNotifications: mockes.notifications.toasts, @@ -122,9 +122,10 @@ describe('alert_edit', () => { await nextTick(); wrapper.update(); }); - }); + } - it('renders alert add flyout', () => { + it('renders alert add flyout', async () => { + await setup(); expect(wrapper.find('[data-test-subj="editAlertFlyoutTitle"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="saveEditedAlertButton"]').exists()).toBeTruthy(); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index 0c22ce0fca80c..bd18c99dca8fb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -4,22 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; import { ValidationResult, Alert } from '../../../types'; import { AlertForm } from './alert_form'; -import { AppDeps } from '../../app'; -import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { AlertsContextProvider } from '../../context/alerts_context'; +import { coreMock } from 'src/core/public/mocks'; const actionTypeRegistry = actionTypeRegistryMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); describe('alert_form', () => { - let deps: AppDeps | null; + let deps: any; const alertType = { id: 'my-alert-type', iconClass: 'test', @@ -44,42 +41,19 @@ describe('alert_form', () => { actionConnectorFields: null, actionParamsFields: null, }; - beforeAll(async () => { - const mockes = coreMock.createSetup(); - const [ - { - chrome, - docLinks, - application: { capabilities }, - }, - ] = await mockes.getStartServices(); - deps = { - chrome, - docLinks, - toastNotifications: mockes.notifications.toasts, - injectedMetadata: mockes.injectedMetadata, - http: mockes.http, - uiSettings: mockes.uiSettings, - dataPlugin: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), - capabilities: { - ...capabilities, - siem: { - 'alerting:show': true, - 'alerting:save': true, - 'alerting:delete': false, - }, - }, - setBreadcrumbs: jest.fn(), - actionTypeRegistry: actionTypeRegistry as any, - alertTypeRegistry: alertTypeRegistry as any, - }; - }); describe('alert_form create alert', () => { let wrapper: ReactWrapper; - beforeAll(async () => { + async function setup() { + const mockes = coreMock.createSetup(); + deps = { + toastNotifications: mockes.notifications.toasts, + http: mockes.http, + uiSettings: mockes.uiSettings, + actionTypeRegistry: actionTypeRegistry as any, + alertTypeRegistry: alertTypeRegistry as any, + }; alertTypeRegistry.list.mockReturnValue([alertType]); alertTypeRegistry.has.mockReturnValue(true); actionTypeRegistry.list.mockReturnValue([actionType]); @@ -99,47 +73,49 @@ describe('alert_form', () => { mutedInstanceIds: [], } as unknown) as Alert; + wrapper = mountWithIntl( + { + return new Promise(() => {}); + }, + http: deps!.http, + actionTypeRegistry: deps!.actionTypeRegistry, + alertTypeRegistry: deps!.alertTypeRegistry, + toastNotifications: deps!.toastNotifications, + uiSettings: deps!.uiSettings, + }} + > + {}} + errors={{ name: [] }} + serverError={null} + /> + + ); + await act(async () => { - if (deps) { - wrapper = mountWithIntl( - { - return new Promise(() => {}); - }, - http: deps.http, - actionTypeRegistry: deps.actionTypeRegistry, - alertTypeRegistry: deps.alertTypeRegistry, - toastNotifications: deps.toastNotifications, - uiSettings: deps.uiSettings, - }} - > - {}} - errors={{ name: [] }} - serverError={null} - /> - - ); - } + await nextTick(); + wrapper.update(); }); + } - await waitForRender(wrapper); - }); - - it('renders alert name', () => { + it('renders alert name', async () => { + await setup(); const alertNameField = wrapper.find('[data-test-subj="alertNameInput"]'); expect(alertNameField.exists()).toBeTruthy(); expect(alertNameField.first().prop('value')).toBe('test'); }); - it('renders registered selected alert type', () => { + it('renders registered selected alert type', async () => { + await setup(); const alertTypeSelectOptions = wrapper.find('[data-test-subj="my-alert-type-SelectOption"]'); expect(alertTypeSelectOptions.exists()).toBeTruthy(); }); - it('renders registered action types', () => { + it('renders registered action types', async () => { + await setup(); const alertTypeSelectOptions = wrapper.find( '[data-test-subj=".server-log-ActionTypeSelectOption"]' ); @@ -150,7 +126,15 @@ describe('alert_form', () => { describe('alert_form edit alert', () => { let wrapper: ReactWrapper; - beforeAll(async () => { + async function setup() { + const mockes = coreMock.createSetup(); + deps = { + toastNotifications: mockes.notifications.toasts, + http: mockes.http, + uiSettings: mockes.uiSettings, + actionTypeRegistry: actionTypeRegistry as any, + alertTypeRegistry: alertTypeRegistry as any, + }; alertTypeRegistry.list.mockReturnValue([alertType]); alertTypeRegistry.get.mockReturnValue(alertType); alertTypeRegistry.has.mockReturnValue(true); @@ -173,57 +157,53 @@ describe('alert_form', () => { mutedInstanceIds: [], } as unknown) as Alert; + wrapper = mountWithIntl( + { + return new Promise(() => {}); + }, + http: deps!.http, + actionTypeRegistry: deps!.actionTypeRegistry, + alertTypeRegistry: deps!.alertTypeRegistry, + toastNotifications: deps!.toastNotifications, + uiSettings: deps!.uiSettings, + }} + > + {}} + errors={{ name: [] }} + serverError={null} + /> + + ); + await act(async () => { - if (deps) { - wrapper = mountWithIntl( - { - return new Promise(() => {}); - }, - http: deps.http, - actionTypeRegistry: deps.actionTypeRegistry, - alertTypeRegistry: deps.alertTypeRegistry, - toastNotifications: deps.toastNotifications, - uiSettings: deps.uiSettings, - }} - > - {}} - errors={{ name: [] }} - serverError={null} - /> - - ); - } + await nextTick(); + wrapper.update(); }); + } - await waitForRender(wrapper); - }); - - it('renders alert name', () => { + it('renders alert name', async () => { + await setup(); const alertNameField = wrapper.find('[data-test-subj="alertNameInput"]'); expect(alertNameField.exists()).toBeTruthy(); expect(alertNameField.first().prop('value')).toBe('test'); }); - it('renders registered selected alert type', () => { + it('renders registered selected alert type', async () => { + await setup(); const alertTypeSelectOptions = wrapper.find('[data-test-subj="selectedAlertTypeTitle"]'); expect(alertTypeSelectOptions.exists()).toBeTruthy(); }); - it('renders registered action types', () => { + it('renders registered action types', async () => { + await setup(); const actionTypeSelectOptions = wrapper.find( '[data-test-subj="my-action-type-ActionTypeSelectOption"]' ); expect(actionTypeSelectOptions.exists()).toBeTruthy(); }); }); - - async function waitForRender(wrapper: ReactWrapper) { - await Promise.resolve(); - await Promise.resolve(); - wrapper.update(); - } }); diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 0be0a919112f8..74af4a77d0ef0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -8,7 +8,12 @@ import { PluginInitializerContext } from 'src/core/public'; import { Plugin } from './plugin'; export { AlertsContextProvider } from './application/context/alerts_context'; +export { ActionsConnectorsContextProvider } from './application/context/actions_connectors_context'; export { AlertAdd } from './application/sections/alert_form'; +export { + ConnectorAddFlyout, + ConnectorEditFlyout, +} from './application/sections/action_connector_form'; export function plugin(ctx: PluginInitializerContext) { return new Plugin(ctx); diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 459197d80d7aa..9f975cba3c0d1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -22,7 +22,10 @@ export interface TriggersAndActionsUIPublicPluginSetup { alertTypeRegistry: TypeRegistry; } -export type Start = void; +export interface TriggersAndActionsUIPublicPluginStart { + actionTypeRegistry: TypeRegistry; + alertTypeRegistry: TypeRegistry; +} interface PluginsStart { data: DataPublicPluginStart; @@ -30,7 +33,9 @@ interface PluginsStart { management: ManagementStart; } -export class Plugin implements CorePlugin { +export class Plugin + implements + CorePlugin { private actionTypeRegistry: TypeRegistry; private alertTypeRegistry: TypeRegistry; @@ -57,44 +62,46 @@ export class Plugin implements CorePlugin { + boot({ + dataPlugin: plugins.data, + charts: plugins.charts, + element: params.element, + toastNotifications: core.notifications.toasts, + injectedMetadata: core.injectedMetadata, + http: core.http, + uiSettings: core.uiSettings, + docLinks: core.docLinks, + chrome: core.chrome, + savedObjects: core.savedObjects.client, + I18nContext: core.i18n.Context, + capabilities: core.application.capabilities, + setBreadcrumbs: params.setBreadcrumbs, + actionTypeRegistry: this.actionTypeRegistry, + alertTypeRegistry: this.alertTypeRegistry, + }); + return () => {}; + }, + }); } - - plugins.management.sections.getSection('kibana')!.registerApp({ - id: 'triggersActions', - title: i18n.translate('xpack.triggersActionsUI.managementSection.displayName', { - defaultMessage: 'Alerts and Actions', - }), - order: 7, - mount: params => { - boot({ - dataPlugin: plugins.data, - charts: plugins.charts, - element: params.element, - toastNotifications: core.notifications.toasts, - injectedMetadata: core.injectedMetadata, - http: core.http, - uiSettings: core.uiSettings, - docLinks: core.docLinks, - chrome: core.chrome, - savedObjects: core.savedObjects.client, - I18nContext: core.i18n.Context, - capabilities: core.application.capabilities, - setBreadcrumbs: params.setBreadcrumbs, - actionTypeRegistry: this.actionTypeRegistry, - alertTypeRegistry: this.alertTypeRegistry, - }); - return () => {}; - }, - }); + return { + actionTypeRegistry: this.actionTypeRegistry, + alertTypeRegistry: this.alertTypeRegistry, + }; } public stop() {} diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 25ebc6d610f86..75ae6b9ea7c21 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -60,7 +60,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('thresholdAlertTimeFieldSelect'); const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); await fieldOptions[1].click(); + // need this two out of popup clicks to close them await nameInput.click(); + await testSubjects.click('intervalInput'); + await testSubjects.click('.slack-ActionTypeSelectOption'); await testSubjects.click('createActionConnectorButton'); const connectorNameInput = await testSubjects.find('nameInput'); From 5408f45b525d53b95375db47281cab410c239113 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 5 Mar 2020 15:59:33 -0700 Subject: [PATCH 12/12] expand max-old-space-size for xpack jest tests (#59455) * expand max-old-space-size for xpack jest tests * turns out we are already at 4GB * limit to 6GB for now --- test/scripts/jenkins_xpack.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index b629e064b39b5..5055997df642a 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -11,7 +11,7 @@ if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> Running jest tests" cd "$XPACK_DIR" - checks-reporter-with-killswitch "X-Pack Jest" node scripts/jest --ci --verbose --detectOpenHandles + checks-reporter-with-killswitch "X-Pack Jest" node --max-old-space-size=6144 scripts/jest --ci --verbose --detectOpenHandles echo "" echo ""