From 2abbd808c743f797363fb1f1d44912d7f376b437 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Sun, 17 Jan 2021 09:18:34 -0700 Subject: [PATCH 01/23] [Security Solutions][Detection Engine] Removes duplicate API calls (#88420) ## Summary This removes some duplicate API calls to reduce pressure on the backend and speed up querying times within the application for the front end. This fixes some of the issues of https://github.com/elastic/kibana/issues/82327, but there are several performance improvements that are going to be needed to help reduce the slowness when you have a system under a lot of pressure. So far this removes duplication for these API calls when you are on the manage detection rules page: ```ts api/detection_engine/rules/_find api/detection_engine/rules/_find_statuses api/detection_engine/tags ``` Screen Shot 2021-01-14 at 3 53 21 PM * This hides the tags and searches while the page is loading to avoid duplicate calls when the pre-packaged rules counts come back * This untangles the refetchRules from the refetchPrePackagedRulesStatus as two separate calls to avoid issues we have with re-rendering and re-calling the backend. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../detection_engine/alerts/use_query.tsx | 2 +- .../alerts/use_signal_index.tsx | 2 +- .../containers/detection_engine/rules/api.ts | 6 +- .../detection_engine/rules/types.ts | 6 +- .../rules/use_pre_packaged_rules.tsx | 2 +- .../rules/use_rule_status.tsx | 4 +- .../detection_engine/rules/use_rules.test.tsx | 15 +++++ .../detection_engine/rules/use_rules.tsx | 25 ++------ .../rules/all/batch_actions.tsx | 14 +++-- .../rules/all/columns.test.tsx | 3 + .../detection_engine/rules/all/columns.tsx | 14 +++-- .../rules/all/exceptions/exceptions_table.tsx | 2 +- .../detection_engine/rules/all/index.tsx | 4 +- .../rules/all/reducer.test.ts | 6 ++ .../rules/all/rules_tables.tsx | 61 +++++++++++-------- .../pages/detection_engine/rules/index.tsx | 10 +-- 16 files changed, 105 insertions(+), 71 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx index 9c992fa872705..3bef1d8edd048 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx @@ -9,7 +9,7 @@ import React, { SetStateAction, useEffect, useState } from 'react'; import { fetchQueryAlerts } from './api'; import { AlertSearchResponse } from './types'; -type Func = () => void; +type Func = () => Promise; export interface ReturnQueryAlerts { loading: boolean; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx index 1233456359b7f..5ebdb38b8dd5c 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx @@ -11,7 +11,7 @@ import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; import { isSecurityAppError } from '../../../../common/utils/api'; -type Func = () => void; +type Func = () => Promise; export interface ReturnSignalIndex { loading: boolean; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index da33b7841c7a9..f602a0a9523c7 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -120,9 +120,9 @@ export const fetchRules = async ({ ...showElasticRuleFilter, ].join(' AND '); - const tags = [ - ...(filterOptions.tags?.map((t) => `alert.attributes.tags: "${t.replace(/"/g, '\\"')}"`) ?? []), - ].join(' AND '); + const tags = filterOptions.tags + .map((t) => `alert.attributes.tags: "${t.replace(/"/g, '\\"')}"`) + .join(' AND '); const filterString = filtersWithoutTags !== '' && tags !== '' diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index b930212610ae9..6eefa7f732bec 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -177,9 +177,9 @@ export interface FilterOptions { filter: string; sortField: RulesSortingFields; sortOrder: SortOrder; - showCustomRules?: boolean; - showElasticRules?: boolean; - tags?: string[]; + showCustomRules: boolean; + showElasticRules: boolean; + tags: string[]; } export interface FetchRulesResponse { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx index 48530ddeb181e..d83d4e0caa977 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -20,7 +20,7 @@ import { getPrePackagedTimelineStatus, } from '../../../pages/detection_engine/rules/helpers'; -type Func = () => void; +type Func = () => Promise; export type CreatePreBuiltRules = () => Promise; interface ReturnPrePackagedTimelines { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx index 0e96f58ee6874..ddf50e9edae51 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx @@ -113,9 +113,11 @@ export const useRulesStatuses = (rules: Rules): ReturnRulesStatuses => { setLoading(false); } }; - if (rules != null && rules.length > 0) { + + if (rules.length > 0) { fetchData(rules.map((r) => r.id)); } + return () => { isSubscribed = false; abortCtrl.abort(); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.test.tsx index 76f2a5b58754e..a874acf36c525 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.test.tsx @@ -27,6 +27,9 @@ describe('useRules', () => { filter: '', sortField: 'created_at', sortOrder: 'desc', + tags: [], + showCustomRules: false, + showElasticRules: false, }, }) ); @@ -48,6 +51,9 @@ describe('useRules', () => { filter: '', sortField: 'created_at', sortOrder: 'desc', + tags: [], + showCustomRules: false, + showElasticRules: false, }, }) ); @@ -153,6 +159,9 @@ describe('useRules', () => { filter: '', sortField: 'created_at', sortOrder: 'desc', + tags: [], + showCustomRules: false, + showElasticRules: false, }, }) ); @@ -182,6 +191,9 @@ describe('useRules', () => { filter: '', sortField: 'created_at', sortOrder: 'desc', + tags: [], + showCustomRules: false, + showElasticRules: false, }, }, } @@ -198,6 +210,9 @@ describe('useRules', () => { filter: 'hello world', sortField: 'created_at', sortOrder: 'desc', + tags: [], + showCustomRules: false, + showElasticRules: false, }, }); await waitForNextUpdate(); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.tsx index 2ada6d8426ce1..9b4a5ce8c23c3 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rules.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { noop } from 'lodash/fp'; import { useEffect, useState, useRef } from 'react'; import { FetchRulesResponse, FilterOptions, PaginationOptions, Rule } from './types'; @@ -12,16 +11,11 @@ import { errorToToaster, useStateToaster } from '../../../../common/components/t import { fetchRules } from './api'; import * as i18n from './translations'; -export type ReturnRules = [ - boolean, - FetchRulesResponse | null, - (refreshPrePackagedRule?: boolean) => void -]; +export type ReturnRules = [boolean, FetchRulesResponse | null, () => Promise]; export interface UseRules { pagination: PaginationOptions; filterOptions: FilterOptions; - refetchPrePackagedRulesStatus?: () => void; dispatchRulesInReducer?: (rules: Rule[], pagination: Partial) => void; } @@ -34,20 +28,19 @@ export interface UseRules { export const useRules = ({ pagination, filterOptions, - refetchPrePackagedRulesStatus, dispatchRulesInReducer, }: UseRules): ReturnRules => { const [rules, setRules] = useState(null); - const reFetchRules = useRef<(refreshPrePackagedRule?: boolean) => void>(noop); + const reFetchRules = useRef<() => Promise>(() => Promise.resolve()); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); - const filterTags = filterOptions.tags?.sort().join(); + const filterTags = filterOptions.tags.sort().join(); useEffect(() => { let isSubscribed = true; const abortCtrl = new AbortController(); - async function fetchData() { + const fetchData = async () => { try { setLoading(true); const fetchRulesResult = await fetchRules({ @@ -77,15 +70,10 @@ export const useRules = ({ if (isSubscribed) { setLoading(false); } - } + }; fetchData(); - reFetchRules.current = (refreshPrePackagedRule: boolean = false) => { - fetchData(); - if (refreshPrePackagedRule && refetchPrePackagedRulesStatus != null) { - refetchPrePackagedRulesStatus(); - } - }; + reFetchRules.current = (): Promise => fetchData(); return () => { isSubscribed = false; abortCtrl.abort(); @@ -100,7 +88,6 @@ export const useRules = ({ filterTags, filterOptions.showCustomRules, filterOptions.showElasticRules, - refetchPrePackagedRulesStatus, ]); return [loading, rules, reFetchRules.current]; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/batch_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/batch_actions.tsx index f911fbddd81c7..1ed534069470b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/batch_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/batch_actions.tsx @@ -27,7 +27,8 @@ interface GetBatchItems { hasMlPermissions: boolean; hasActionsPrivileges: boolean; loadingRuleIds: string[]; - reFetchRules: (refreshPrePackagedRule?: boolean) => void; + reFetchRules: () => Promise; + refetchPrePackagedRulesStatus: () => Promise; rules: Rule[]; selectedRuleIds: string[]; } @@ -39,17 +40,18 @@ export const getBatchItems = ({ hasMlPermissions, loadingRuleIds, reFetchRules, + refetchPrePackagedRulesStatus, rules, selectedRuleIds, hasActionsPrivileges, }: GetBatchItems) => { - const selectedRules = selectedRuleIds.reduce((acc, id) => { + const selectedRules = selectedRuleIds.reduce>((acc, id) => { const found = rules.find((r) => r.id === id); if (found != null) { return { [id]: found, ...acc }; } return acc; - }, {} as Record); + }, {}); const containsEnabled = selectedRuleIds.some((id) => selectedRules[id]?.enabled ?? false); const containsDisabled = selectedRuleIds.some((id) => !selectedRules[id]?.enabled ?? false); @@ -139,7 +141,8 @@ export const getBatchItems = ({ dispatch, dispatchToaster ); - reFetchRules(true); + await reFetchRules(); + await refetchPrePackagedRulesStatus(); }} > { closePopover(); await deleteRulesAction(selectedRuleIds, dispatch, dispatchToaster); - reFetchRules(true); + await reFetchRules(); + await refetchPrePackagedRulesStatus(); }} > {i18n.BATCH_ACTION_DELETE_SELECTED} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx index 564b382b7b29f..c48ba49e8db2b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx @@ -27,6 +27,7 @@ describe('AllRulesTable Columns', () => { const dispatch = jest.fn(); const dispatchToaster = jest.fn(); const reFetchRules = jest.fn(); + const refetchPrePackagedRulesStatus = jest.fn(); beforeEach(() => { results = []; @@ -53,6 +54,7 @@ describe('AllRulesTable Columns', () => { dispatchToaster, history, reFetchRules, + refetchPrePackagedRulesStatus, true )[1]; await duplicateRulesActionObject.onClick(rule); @@ -75,6 +77,7 @@ describe('AllRulesTable Columns', () => { dispatchToaster, history, reFetchRules, + refetchPrePackagedRulesStatus, true )[3]; await deleteRulesActionObject.onClick(rule); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx index 2b03d6dd4de36..0d585b4463815 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx @@ -43,7 +43,8 @@ export const getActions = ( dispatch: React.Dispatch, dispatchToaster: Dispatch, history: H.History, - reFetchRules: (refreshPrePackagedRule?: boolean) => void, + reFetchRules: () => Promise, + refetchPrePackagedRulesStatus: () => Promise, actionsPrivileges: | boolean | Readonly<{ @@ -77,7 +78,8 @@ export const getActions = ( enabled: (rowItem: Rule) => canEditRuleWithActions(rowItem, actionsPrivileges), onClick: async (rowItem: Rule) => { await duplicateRulesAction([rowItem], [rowItem.id], dispatch, dispatchToaster); - await reFetchRules(true); + await reFetchRules(); + await refetchPrePackagedRulesStatus(); }, }, { @@ -95,7 +97,8 @@ export const getActions = ( name: i18n.DELETE_RULE, onClick: async (rowItem: Rule) => { await deleteRulesAction([rowItem.id], dispatch, dispatchToaster); - await reFetchRules(true); + await reFetchRules(); + await refetchPrePackagedRulesStatus(); }, }, ]; @@ -115,7 +118,8 @@ interface GetColumns { hasMlPermissions: boolean; hasNoPermissions: boolean; loadingRuleIds: string[]; - reFetchRules: (refreshPrePackagedRule?: boolean) => void; + reFetchRules: () => Promise; + refetchPrePackagedRulesStatus: () => Promise; hasReadActionsPrivileges: | boolean | Readonly<{ @@ -132,6 +136,7 @@ export const getColumns = ({ hasNoPermissions, loadingRuleIds, reFetchRules, + refetchPrePackagedRulesStatus, hasReadActionsPrivileges, }: GetColumns): RulesColumns[] => { const cols: RulesColumns[] = [ @@ -279,6 +284,7 @@ export const getColumns = ({ dispatchToaster, history, reFetchRules, + refetchPrePackagedRulesStatus, hasReadActionsPrivileges ), width: '40px', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index ccd00daf5e5aa..cc04c205abce8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -36,7 +36,7 @@ import { patchRule } from '../../../../../containers/detection_engine/rules/api' // eslint-disable-next-line @typescript-eslint/no-explicit-any const MyEuiBasicTable = styled(EuiBasicTable as any)`` as any; -export type Func = () => void; +export type Func = () => Promise; export interface ExceptionListFilter { name?: string | null; list_id?: string | null; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx index 4c4095ee6f740..381f104855bf3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx @@ -20,12 +20,12 @@ interface AllRulesProps { hasNoPermissions: boolean; loading: boolean; loadingCreatePrePackagedRules: boolean; - refetchPrePackagedRulesStatus: () => void; + refetchPrePackagedRulesStatus: () => Promise; rulesCustomInstalled: number | null; rulesInstalled: number | null; rulesNotInstalled: number | null; rulesNotUpdated: number | null; - setRefreshRulesData: (refreshRule: (refreshPrePackagedRule?: boolean) => void) => void; + setRefreshRulesData: (refreshRule: () => Promise) => void; } export enum AllRulesTabs { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.test.ts index 0456111074b60..7501f4377740e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.test.ts @@ -14,6 +14,9 @@ const initialState: State = { filter: '', sortField: 'enabled', sortOrder: 'desc', + tags: [], + showCustomRules: false, + showElasticRules: false, }, loadingRuleIds: [], loadingRulesAction: null, @@ -193,6 +196,9 @@ describe('allRulesReducer', () => { filter: 'host.name:*', sortField: 'enabled', sortOrder: 'desc', + tags: [], + showCustomRules: false, + showElasticRules: false, }; const { filterOptions, pagination } = reducer(initialState, { type: 'updateFilterOptions', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx index 232fb118fb2f7..2ae124dc86104 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx @@ -60,6 +60,9 @@ const initialState: State = { filter: '', sortField: INITIAL_SORT_FIELD, sortOrder: 'desc', + tags: [], + showCustomRules: false, + showElasticRules: false, }, loadingRuleIds: [], loadingRulesAction: null, @@ -82,12 +85,12 @@ interface RulesTableProps { hasNoPermissions: boolean; loading: boolean; loadingCreatePrePackagedRules: boolean; - refetchPrePackagedRulesStatus: () => void; + refetchPrePackagedRulesStatus: () => Promise; rulesCustomInstalled: number | null; rulesInstalled: number | null; rulesNotInstalled: number | null; rulesNotUpdated: number | null; - setRefreshRulesData: (refreshRule: (refreshPrePackagedRule?: boolean) => void) => void; + setRefreshRulesData: (refreshRule: () => Promise) => void; selectedTab: AllRulesTabs; } @@ -183,10 +186,9 @@ export const RulesTables = React.memo( }); }, []); - const [isLoadingRules, , reFetchRulesData] = useRules({ + const [isLoadingRules, , reFetchRules] = useRules({ pagination, filterOptions, - refetchPrePackagedRulesStatus, dispatchRulesInReducer: setRules, }); @@ -220,7 +222,8 @@ export const RulesTables = React.memo( hasActionsPrivileges, loadingRuleIds, selectedRuleIds, - reFetchRules: reFetchRulesData, + reFetchRules, + refetchPrePackagedRulesStatus, rules, }); }, @@ -229,7 +232,8 @@ export const RulesTables = React.memo( dispatchToaster, hasMlPermissions, loadingRuleIds, - reFetchRulesData, + reFetchRules, + refetchPrePackagedRulesStatus, rules, selectedRuleIds, hasActionsPrivileges, @@ -273,19 +277,22 @@ export const RulesTables = React.memo( (loadingRulesAction === 'enable' || loadingRulesAction === 'disable') ? loadingRuleIds : [], - reFetchRules: reFetchRulesData, + reFetchRules, + refetchPrePackagedRulesStatus, hasReadActionsPrivileges: hasActionsPrivileges, }); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ dispatch, dispatchToaster, formatUrl, + refetchPrePackagedRulesStatus, + hasActionsPrivileges, + hasNoPermissions, hasMlPermissions, history, loadingRuleIds, loadingRulesAction, - reFetchRulesData, + reFetchRules, ]); const monitoringColumns = useMemo(() => getMonitoringColumns(history, formatUrl), [ @@ -294,10 +301,8 @@ export const RulesTables = React.memo( ]); useEffect(() => { - if (reFetchRulesData != null) { - setRefreshRulesData(reFetchRulesData); - } - }, [reFetchRulesData, setRefreshRulesData]); + setRefreshRulesData(reFetchRules); + }, [reFetchRules, setRefreshRulesData]); useEffect(() => { if (initLoading && !loading && !isLoadingRules && !isLoadingRulesStatuses) { @@ -306,11 +311,12 @@ export const RulesTables = React.memo( }, [initLoading, loading, isLoadingRules, isLoadingRulesStatuses]); const handleCreatePrePackagedRules = useCallback(async () => { - if (createPrePackagedRules != null && reFetchRulesData != null) { + if (createPrePackagedRules != null) { await createPrePackagedRules(); - reFetchRulesData(true); + await reFetchRules(); + await refetchPrePackagedRulesStatus(); } - }, [createPrePackagedRules, reFetchRulesData]); + }, [createPrePackagedRules, reFetchRules, refetchPrePackagedRulesStatus]); const euiBasicTableSelectionProps = useMemo( () => ({ @@ -343,12 +349,13 @@ export const RulesTables = React.memo( return false; }, [loadingRuleIds, loadingRulesAction]); - const handleRefreshData = useCallback((): void => { - if (reFetchRulesData != null && !isLoadingAnActionOnRule) { - reFetchRulesData(true); + const handleRefreshData = useCallback(async (): Promise => { + if (!isLoadingAnActionOnRule) { + await reFetchRules(); + await refetchPrePackagedRulesStatus(); setLastRefreshDate(); } - }, [reFetchRulesData, isLoadingAnActionOnRule, setLastRefreshDate]); + }, [reFetchRules, isLoadingAnActionOnRule, setLastRefreshDate, refetchPrePackagedRulesStatus]); const handleResetIdleTimer = useCallback((): void => { if (isRefreshOn) { @@ -458,12 +465,14 @@ export const RulesTables = React.memo( /> } > - + {shouldShowRulesTable && ( + + )} {isLoadingAnActionOnRule && !initLoading && ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index dc2e99b90da40..9423604e546e9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -35,7 +35,7 @@ import { SecurityPageName } from '../../../../app/types'; import { LinkButton } from '../../../../common/components/links'; import { useFormatUrl } from '../../../../common/components/link_to'; -type Func = (refreshPrePackagedRule?: boolean) => void; +type Func = () => Promise; const RulesPageComponent: React.FC = () => { const history = useHistory(); @@ -94,20 +94,22 @@ const RulesPageComponent: React.FC = () => { const handleRefreshRules = useCallback(async () => { if (refreshRulesData.current != null) { - refreshRulesData.current(true); + await refreshRulesData.current(); } }, [refreshRulesData]); const handleCreatePrePackagedRules = useCallback(async () => { if (createPrePackagedRules != null) { await createPrePackagedRules(); - handleRefreshRules(); + return handleRefreshRules(); } }, [createPrePackagedRules, handleRefreshRules]); const handleRefetchPrePackagedRulesStatus = useCallback(() => { if (refetchPrePackagedRulesStatus != null) { - refetchPrePackagedRulesStatus(); + return refetchPrePackagedRulesStatus(); + } else { + return Promise.resolve(); } }, [refetchPrePackagedRulesStatus]); From f7fdda5db7e41b04f0f37cc180b441e8ed0c9321 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 18 Jan 2021 09:59:26 +0200 Subject: [PATCH 02/23] [Security Solution][Case] Fix patch cases integration test with alerts (#88311) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../basic/tests/cases/patch_cases.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts index 5b12ba4bf613f..42012e72a4033 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts @@ -35,9 +35,7 @@ export default ({ getService }: FtrProviderContext): void => { const esArchiver = getService('esArchiver'); const es = getService('es'); - // Failing: See https://github.com/elastic/kibana/issues/88130 - // FLAKY: https://github.com/elastic/kibana/issues/87988 - describe.skip('patch_cases', () => { + describe('patch_cases', () => { afterEach(async () => { await deleteCases(es); await deleteCasesUserActions(es); @@ -277,7 +275,8 @@ export default ({ getService }: FtrProviderContext): void => { await esArchiver.unload('auditbeat/hosts'); }); - it('updates alert status when the status is updated and syncAlerts=true', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/87988 + it.skip('updates alert status when the status is updated and syncAlerts=true', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); const { body: postedCase } = await supertest @@ -377,7 +376,8 @@ export default ({ getService }: FtrProviderContext): void => { expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open'); }); - it('it updates alert status when syncAlerts is turned on', async () => { + // Failing: See https://github.com/elastic/kibana/issues/88130 + it.skip('it updates alert status when syncAlerts is turned on', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); const { body: postedCase } = await supertest From f0d20097a1816dc4103cf1a9427e2afb8e098b42 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Mon, 18 Jan 2021 13:53:27 +0100 Subject: [PATCH 03/23] [ML] Disable a11y tests --- x-pack/test/accessibility/apps/ml.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/ml.ts b/x-pack/test/accessibility/apps/ml.ts index 90c583842a360..799911cd77a9f 100644 --- a/x-pack/test/accessibility/apps/ml.ts +++ b/x-pack/test/accessibility/apps/ml.ts @@ -12,7 +12,8 @@ export default function ({ getService }: FtrProviderContext) { const a11y = getService('a11y'); const ml = getService('ml'); - describe('ml', () => { + // flaky tests, see https://github.com/elastic/kibana/issues/88592 + describe.skip('ml', () => { const esArchiver = getService('esArchiver'); before(async () => { From d55ff819140f744f9f9c95b19aa44adbd0f7b504 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 18 Jan 2021 14:47:36 +0100 Subject: [PATCH 04/23] [Search Sessions] Fix restoration URL conflicts with global time sync (#87488) --- src/plugins/dashboard/public/plugin.tsx | 14 ++++++++++++ src/plugins/discover/public/plugin.ts | 15 +++++++++++++ src/plugins/kibana_utils/public/index.ts | 2 ++ .../public/state_management/url/index.ts | 1 + .../url/kbn_url_tracker.test.ts | 22 ++++++++++++++++++- .../state_management/url/kbn_url_tracker.ts | 17 ++++++++++++++ 6 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 166f4eaf39997..a65f1b3ea53a6 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -84,6 +84,7 @@ import { PlaceholderEmbeddableFactory } from './application/embeddable/placehold import { UrlGeneratorState } from '../../share/public'; import { ExportCSVAction } from './application/actions/export_csv_action'; import { dashboardFeatureCatalog } from './dashboard_strings'; +import { replaceUrlHashQuery } from '../../kibana_utils/public'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -250,6 +251,19 @@ export class DashboardPlugin }, ], getHistory: () => this.currentHistory!, + onBeforeNavLinkSaved: (newNavLink: string) => { + // Do not save SEARCH_SESSION_ID into nav link, because of possible edge cases + // that could lead to session restoration failure. + // see: https://github.com/elastic/kibana/issues/87149 + if (newNavLink.includes(DashboardConstants.SEARCH_SESSION_ID)) { + newNavLink = replaceUrlHashQuery(newNavLink, (query) => { + delete query[DashboardConstants.SEARCH_SESSION_ID]; + return query; + }); + } + + return newNavLink; + }, }); const dashboardContainerFactory = new DashboardContainerFactoryDefinition(getStartServices); diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index 647746b98cbd1..827bcfbbe8017 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -67,9 +67,11 @@ import { DiscoverUrlGeneratorState, DISCOVER_APP_URL_GENERATOR, DiscoverUrlGenerator, + SEARCH_SESSION_ID_QUERY_PARAM, } from './url_generator'; import { SearchEmbeddableFactory } from './application/embeddable'; import { UsageCollectionSetup } from '../../usage_collection/public'; +import { replaceUrlHashQuery } from '../../kibana_utils/public/'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -229,6 +231,19 @@ export class DiscoverPlugin ), }, ], + onBeforeNavLinkSaved: (newNavLink: string) => { + // Do not save SEARCH_SESSION_ID into nav link, because of possible edge cases + // that could lead to session restoration failure. + // see: https://github.com/elastic/kibana/issues/87149 + if (newNavLink.includes(SEARCH_SESSION_ID_QUERY_PARAM)) { + newNavLink = replaceUrlHashQuery(newNavLink, (query) => { + delete query[SEARCH_SESSION_ID_QUERY_PARAM]; + return query; + }); + } + + return newNavLink; + }, }); setUrlTracker({ setTrackedUrl, restorePreviousUrl }); this.stopUrlTracking = () => { diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 46a0cc4a10f00..fad2b8e438449 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -62,6 +62,8 @@ export { getStatesFromKbnUrl, setStateToKbnUrl, withNotifyOnErrors, + replaceUrlQuery, + replaceUrlHashQuery, } from './state_management/url'; export { syncState, diff --git a/src/plugins/kibana_utils/public/state_management/url/index.ts b/src/plugins/kibana_utils/public/state_management/url/index.ts index 66fecd723e3ba..f1c745b900673 100644 --- a/src/plugins/kibana_utils/public/state_management/url/index.ts +++ b/src/plugins/kibana_utils/public/state_management/url/index.ts @@ -28,3 +28,4 @@ export { export { createKbnUrlTracker } from './kbn_url_tracker'; export { createUrlTracker } from './url_tracker'; export { withNotifyOnErrors, saveStateInUrlErrorTitle, restoreUrlErrorTitle } from './errors'; +export { replaceUrlHashQuery, replaceUrlQuery } from './format'; diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts index 2a50fa7a70247..778266c8aed17 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts @@ -37,6 +37,7 @@ describe('kbnUrlTracker', () => { let state2Subject: Subject<{ key2: string }>; let navLinkUpdaterSubject: BehaviorSubject; let toastService: jest.Mocked; + const onBeforeNavLinkSaved = jest.fn((url) => url); function createTracker(shouldTrackUrlUpdate?: (pathname: string) => boolean) { urlTracker = createKbnUrlTracker({ @@ -58,6 +59,7 @@ describe('kbnUrlTracker', () => { navLinkUpdater$: navLinkUpdaterSubject, toastNotifications: toastService, shouldTrackUrlUpdate, + onBeforeNavLinkSaved, }); } @@ -101,6 +103,14 @@ describe('kbnUrlTracker', () => { expect(getActiveNavLinkUrl()).toEqual('#/start'); }); + test('save current URL to storage when app is mounted', () => { + history.push('#/start/deep/path/2'); + createTracker(); + urlTracker.appMounted(); + expect(storage.getItem('storageKey')).toBe('#/start/deep/path/2'); + expect(getActiveNavLinkUrl()).toEqual('#/start'); + }); + test('change nav link to last visited url within app after unmount', () => { createTracker(); urlTracker.appMounted(); @@ -117,7 +127,7 @@ describe('kbnUrlTracker', () => { history.push('#/start/deep/path/2'); history.push('#/start/deep/path/3'); urlTracker.appUnMounted(); - expect(unhashUrl).toHaveBeenCalledTimes(2); + expect(unhashUrl).toHaveBeenCalledTimes(3); // from initial mount + two subsequent location changes expect(getActiveNavLinkUrl()).toEqual('#/start/deep/path/3?unhashed'); }); @@ -215,4 +225,14 @@ describe('kbnUrlTracker', () => { expect(getActiveNavLinkUrl()).toEqual('#/start/deep/path'); }); }); + + describe('onBeforeNavLinkSaved', () => { + test('onBeforeNavLinkSaved saves changed URL', () => { + createTracker(); + urlTracker.appMounted(); + onBeforeNavLinkSaved.mockImplementationOnce(() => 'new_url'); + history.push('#/start/deep/path?state1=(key1:abc)'); + expect(storage.getItem('storageKey')).toEqual('new_url'); + }); + }); }); diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts index ce00b2bf68d93..27fffac564f0a 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts @@ -67,6 +67,7 @@ export function createKbnUrlTracker({ shouldTrackUrlUpdate = () => { return true; }, + onBeforeNavLinkSaved = (newNavLink) => newNavLink, }: { /** * Base url of the current app. This will be used as a prefix for the @@ -123,6 +124,12 @@ export function createKbnUrlTracker({ * @param {string} pathname A location's pathname which comes to history listener */ shouldTrackUrlUpdate?: (pathname: string) => boolean; + + /** + * Called when current subpath is about to be saved to sessionStorage for subsequent use as a nav link. + * Use to mutate app's subpath before it is saved by returning a new subpath. + */ + onBeforeNavLinkSaved?: (newNavLink: string) => string; }): KbnUrlTracker { const storageInstance = storage || sessionStorage; @@ -165,12 +172,19 @@ export function createKbnUrlTracker({ previousActiveUrl = activeUrl; activeUrl = getActiveSubUrl(urlWithStates || urlWithHashes); + activeUrl = onBeforeNavLinkSaved(activeUrl); storageInstance.setItem(storageKey, activeUrl); } function onMountApp() { unsubscribe(); const historyInstance = history || (getHistory && getHistory()) || createHashHistory(); + + // set mounted URL as active + if (shouldTrackUrlUpdate(historyInstance.location.hash)) { + setActiveUrl(historyInstance.location.hash.substr(1)); + } + // track current hash when within app unsubscribeURLHistory = historyInstance.listen((location) => { if (shouldTrackUrlUpdate(location.hash)) { @@ -193,6 +207,9 @@ export function createKbnUrlTracker({ previousActiveUrl = activeUrl; // remove baseUrl prefix (just storing the sub url part) activeUrl = getActiveSubUrl(updatedUrl); + // allow app to mutate resulting URL before committing + activeUrl = onBeforeNavLinkSaved(activeUrl); + storageInstance.setItem(storageKey, activeUrl); setNavLink(activeUrl); }) From b8d43139f3b0e9ccf97e8938b91d5928df17e825 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 18 Jan 2021 15:05:47 +0100 Subject: [PATCH 05/23] [Search Sessions] fix flaky relative time range test (#88359) --- .../async_search/send_to_background_relative_time.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts index 2234ca3e3b034..9eb42b74668c8 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts @@ -24,13 +24,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const pieChart = getService('pieChart'); const find = getService('find'); const dashboardExpect = getService('dashboardExpect'); - const queryBar = getService('queryBar'); const browser = getService('browser'); const sendToBackground = getService('sendToBackground'); describe('send to background with relative time', () => { before(async () => { - await PageObjects.common.sleep(5000); // this part was copied from `x-pack/test/functional/apps/dashboard/_async_dashboard.ts` and this was sleep was needed because of flakiness await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, }); @@ -56,9 +54,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('Saves and restores a session with relative time ranges', async () => { await PageObjects.dashboard.loadSavedDashboard('[Flights] Global Flight Dashboard'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); await PageObjects.timePicker.pauseAutoRefresh(); // sample data has auto-refresh on - await queryBar.submitQuery(); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.dashboard.waitForRenderComplete(); await checkSampleDashboardLoaded(); From 53f4b21a8158d0becbd7874f75c0d3a85ceefbe5 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Mon, 18 Jan 2021 14:12:29 +0000 Subject: [PATCH 06/23] [Logs UI] Add sorting capabilities to categories page (#88051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add sorting capabilities to categories page Co-authored-by: Felix Stürmer --- .../results/log_entry_categories.ts | 19 +++++++++++++++ .../page_results_content.tsx | 11 ++++++++- .../top_categories/top_categories_section.tsx | 7 ++++++ .../top_categories/top_categories_table.tsx | 22 ++++++++++++++++- .../get_top_log_entry_categories.ts | 5 +++- .../use_log_entry_categories_results.ts | 13 +++++++++- .../log_entry_categories_analysis.ts | 13 ++++++---- .../queries/top_log_entry_categories.ts | 24 +++++++++++++++++-- .../results/log_entry_categories.ts | 4 +++- 9 files changed, 107 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_categories.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_categories.ts index f56462012d2e9..0554192398fc5 100644 --- a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_categories.ts +++ b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_categories.ts @@ -30,6 +30,23 @@ export type LogEntryCategoriesHistogramParameters = rt.TypeOf< typeof logEntryCategoriesHistogramParametersRT >; +const sortOptionsRT = rt.keyof({ + maximumAnomalyScore: null, + logEntryCount: null, +}); + +const sortDirectionsRT = rt.keyof({ + asc: null, + desc: null, +}); + +const categorySortRT = rt.type({ + field: sortOptionsRT, + direction: sortDirectionsRT, +}); + +export type CategorySort = rt.TypeOf; + export const getLogEntryCategoriesRequestPayloadRT = rt.type({ data: rt.intersection([ rt.type({ @@ -41,6 +58,8 @@ export const getLogEntryCategoriesRequestPayloadRT = rt.type({ timeRange: timeRangeRT, // a list of histograms to create histograms: rt.array(logEntryCategoriesHistogramParametersRT), + // the criteria to the categories by + sort: categorySortRT, }), rt.partial({ // the datasets to filter for (optional, unfiltered if not present) diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx index 6fc9ce3d8983e..5c1e8f2bdcfcc 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx @@ -87,6 +87,8 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent { getTopLogEntryCategories(); - }, [getTopLogEntryCategories, categoryQueryDatasets, categoryQueryTimeRange.lastChangedTime]); + }, [ + getTopLogEntryCategories, + categoryQueryDatasets, + categoryQueryTimeRange.lastChangedTime, + sortOptions, + ]); useEffect(() => { getLogEntryCategoryDatasets(); @@ -219,6 +226,8 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx index 238300c1a1fb4..c7a6c89012a3a 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_section.tsx @@ -16,6 +16,7 @@ import { RecreateJobButton } from '../../../../../components/logging/log_analysi import { AnalyzeInMlButton } from '../../../../../components/logging/log_analysis_results'; import { DatasetsSelector } from '../../../../../components/logging/log_analysis_results/datasets_selector'; import { TopCategoriesTable } from './top_categories_table'; +import { SortOptions, ChangeSortOptions } from '../../use_log_entry_categories_results'; export const TopCategoriesSection: React.FunctionComponent<{ availableDatasets: string[]; @@ -29,6 +30,8 @@ export const TopCategoriesSection: React.FunctionComponent<{ sourceId: string; timeRange: TimeRange; topCategories: LogEntryCategory[]; + sortOptions: SortOptions; + changeSortOptions: ChangeSortOptions; }> = ({ availableDatasets, hasSetupCapabilities, @@ -41,6 +44,8 @@ export const TopCategoriesSection: React.FunctionComponent<{ sourceId, timeRange, topCategories, + sortOptions, + changeSortOptions, }) => { return ( <> @@ -80,6 +85,8 @@ export const TopCategoriesSection: React.FunctionComponent<{ sourceId={sourceId} timeRange={timeRange} topCategories={topCategories} + sortOptions={sortOptions} + changeSortOptions={changeSortOptions} /> diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx index ab4195492a706..96abe4ab42669 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/top_categories_table.tsx @@ -7,7 +7,7 @@ import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback } from 'react'; import useSet from 'react-use/lib/useSet'; import { euiStyled } from '../../../../../../../observability/public'; @@ -24,6 +24,7 @@ import { RegularExpressionRepresentation } from './category_expression'; import { DatasetActionsList } from './datasets_action_list'; import { DatasetsList } from './datasets_list'; import { LogEntryCountSparkline } from './log_entry_count_sparkline'; +import { SortOptions, ChangeSortOptions } from '../../use_log_entry_categories_results'; export const TopCategoriesTable = euiStyled( ({ @@ -32,13 +33,28 @@ export const TopCategoriesTable = euiStyled( sourceId, timeRange, topCategories, + sortOptions, + changeSortOptions, }: { categorizationJobId: string; className?: string; sourceId: string; timeRange: TimeRange; topCategories: LogEntryCategory[]; + sortOptions: SortOptions; + changeSortOptions: ChangeSortOptions; }) => { + const tableSortOptions = useMemo(() => { + return { sort: sortOptions }; + }, [sortOptions]); + + const handleTableChange = useCallback( + ({ sort = {} }) => { + changeSortOptions(sort); + }, + [changeSortOptions] + ); + const [expandedCategories, { add: expandCategory, remove: collapseCategory }] = useSet( new Set() ); @@ -80,6 +96,8 @@ export const TopCategoriesTable = euiStyled( itemId="categoryId" items={topCategories} rowProps={{ className: `${className} euiTableRow--topAligned` }} + onChange={handleTableChange} + sorting={tableSortOptions} /> ); } @@ -102,6 +120,7 @@ const createColumns = ( name: i18n.translate('xpack.infra.logs.logEntryCategories.countColumnTitle', { defaultMessage: 'Message count', }), + sortable: true, render: (logEntryCount: number) => { return numeral(logEntryCount).format('0,0'); }, @@ -147,6 +166,7 @@ const createColumns = ( name: i18n.translate('xpack.infra.logs.logEntryCategories.maximumAnomalyScoreColumnTitle', { defaultMessage: 'Maximum anomaly score', }), + sortable: true, render: (_maximumAnomalyScore: number, item) => ( ), diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/service_calls/get_top_log_entry_categories.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/service_calls/get_top_log_entry_categories.ts index fd53803796339..a0eaecf04fa4b 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/service_calls/get_top_log_entry_categories.ts +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/service_calls/get_top_log_entry_categories.ts @@ -10,6 +10,7 @@ import { getLogEntryCategoriesRequestPayloadRT, getLogEntryCategoriesSuccessReponsePayloadRT, LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORIES_PATH, + CategorySort, } from '../../../../../common/http_api/log_analysis'; import { decodeOrThrow } from '../../../../../common/runtime_types'; @@ -19,13 +20,14 @@ interface RequestArgs { endTime: number; categoryCount: number; datasets?: string[]; + sort: CategorySort; } export const callGetTopLogEntryCategoriesAPI = async ( requestArgs: RequestArgs, fetch: HttpHandler ) => { - const { sourceId, startTime, endTime, categoryCount, datasets } = requestArgs; + const { sourceId, startTime, endTime, categoryCount, datasets, sort } = requestArgs; const intervalDuration = endTime - startTime; const response = await fetch(LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORIES_PATH, { @@ -58,6 +60,7 @@ export const callGetTopLogEntryCategoriesAPI = async ( bucketCount: 1, }, ], + sort, }, }) ), diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results.ts index 9f193d796e8e8..a64b73dea25e6 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results.ts +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_results.ts @@ -9,6 +9,7 @@ import { useMemo, useState } from 'react'; import { GetLogEntryCategoriesSuccessResponsePayload, GetLogEntryCategoryDatasetsSuccessResponsePayload, + CategorySort, } from '../../../../common/http_api/log_analysis'; import { useTrackedPromise, CanceledPromiseError } from '../../../utils/use_tracked_promise'; import { callGetTopLogEntryCategoriesAPI } from './service_calls/get_top_log_entry_categories'; @@ -18,6 +19,9 @@ import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; type TopLogEntryCategories = GetLogEntryCategoriesSuccessResponsePayload['data']['categories']; type LogEntryCategoryDatasets = GetLogEntryCategoryDatasetsSuccessResponsePayload['data']['datasets']; +export type SortOptions = CategorySort; +export type ChangeSortOptions = (sortOptions: CategorySort) => void; + export const useLogEntryCategoriesResults = ({ categoriesCount, filteredDatasets: filteredDatasets, @@ -35,6 +39,10 @@ export const useLogEntryCategoriesResults = ({ sourceId: string; startTime: number; }) => { + const [sortOptions, setSortOptions] = useState({ + field: 'maximumAnomalyScore', + direction: 'desc', + }); const { services } = useKibanaContextForPlugin(); const [topLogEntryCategories, setTopLogEntryCategories] = useState([]); const [ @@ -53,6 +61,7 @@ export const useLogEntryCategoriesResults = ({ endTime, categoryCount: categoriesCount, datasets: filteredDatasets, + sort: sortOptions, }, services.http.fetch ); @@ -70,7 +79,7 @@ export const useLogEntryCategoriesResults = ({ } }, }, - [categoriesCount, endTime, filteredDatasets, sourceId, startTime] + [categoriesCount, endTime, filteredDatasets, sourceId, startTime, sortOptions] ); const [getLogEntryCategoryDatasetsRequest, getLogEntryCategoryDatasets] = useTrackedPromise( @@ -121,5 +130,7 @@ export const useLogEntryCategoriesResults = ({ isLoadingTopLogEntryCategories, logEntryCategoryDatasets, topLogEntryCategories, + sortOptions, + changeSortOptions: setSortOptions, }; }; diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts index cf3abc81e97ce..7dd5aae9784f5 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts @@ -12,6 +12,7 @@ import { jobCustomSettingsRT, logEntryCategoriesJobTypes, } from '../../../common/log_analysis'; +import { CategorySort } from '../../../common/http_api/log_analysis'; import { startTracingSpan } from '../../../common/performance_tracing'; import { decodeOrThrow } from '../../../common/runtime_types'; import type { MlAnomalyDetectors, MlSystem } from '../../types'; @@ -49,7 +50,8 @@ export async function getTopLogEntryCategories( endTime: number, categoryCount: number, datasets: string[], - histograms: HistogramParameters[] + histograms: HistogramParameters[], + sort: CategorySort ) { const finalizeTopLogEntryCategoriesSpan = startTracingSpan('get top categories'); @@ -68,7 +70,8 @@ export async function getTopLogEntryCategories( startTime, endTime, categoryCount, - datasets + datasets, + sort ); const categoryIds = topLogEntryCategories.map(({ categoryId }) => categoryId); @@ -214,7 +217,8 @@ async function fetchTopLogEntryCategories( startTime: number, endTime: number, categoryCount: number, - datasets: string[] + datasets: string[], + sort: CategorySort ) { const finalizeEsSearchSpan = startTracingSpan('Fetch top categories from ES'); @@ -225,7 +229,8 @@ async function fetchTopLogEntryCategories( startTime, endTime, categoryCount, - datasets + datasets, + sort ), [logEntryCategoriesCountJobId] ) diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts index 5d3d9bc8b4036..057054b427227 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts @@ -14,13 +14,33 @@ import { createDatasetsFilters, } from './common'; +import { CategorySort } from '../../../../common/http_api/log_analysis'; + +type CategoryAggregationOrder = + | 'filter_record>maximum_record_score' + | 'filter_model_plot>sum_actual'; +const getAggregationOrderForSortField = ( + field: CategorySort['field'] +): CategoryAggregationOrder => { + switch (field) { + case 'maximumAnomalyScore': + return 'filter_record>maximum_record_score'; + break; + case 'logEntryCount': + return 'filter_model_plot>sum_actual'; + break; + default: + return 'filter_model_plot>sum_actual'; + } +}; + export const createTopLogEntryCategoriesQuery = ( logEntryCategoriesJobId: string, startTime: number, endTime: number, size: number, datasets: string[], - sortDirection: 'asc' | 'desc' = 'desc' + sort: CategorySort ) => ({ ...defaultRequestParameters, body: { @@ -65,7 +85,7 @@ export const createTopLogEntryCategoriesQuery = ( field: 'by_field_value', size, order: { - 'filter_model_plot>sum_actual': sortDirection, + [getAggregationOrderForSortField(sort.field)]: sort.direction, }, }, aggs: { diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts index 7071d38dffe5d..da5466ed46f6e 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts @@ -33,6 +33,7 @@ export const initGetLogEntryCategoriesRoute = ({ framework }: InfraBackendLibs) sourceId, timeRange: { startTime, endTime }, datasets, + sort, }, } = request.body; @@ -51,7 +52,8 @@ export const initGetLogEntryCategoriesRoute = ({ framework }: InfraBackendLibs) endTime: histogram.timeRange.endTime, id: histogram.id, startTime: histogram.timeRange.startTime, - })) + })), + sort ); return response.ok({ From 898385c2e230e665dc9e5ca9ee231f446213ade1 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 18 Jan 2021 16:58:23 +0100 Subject: [PATCH 07/23] [Lens] Clean full reference operation error state when switching to other operation (#87064) * :bug: Make sure to check incomplete columns before saved ones * :alembic: Try a different approach * :bug: Isolate the fullRef to field transition and trigger the removal + tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../config_panel/layer_panel.test.tsx | 2 +- .../editor_frame/config_panel/layer_panel.tsx | 31 +++++-- .../dimension_panel/dimension_editor.tsx | 9 +- .../dimension_panel/dimension_panel.test.tsx | 82 ++++++++++++++----- .../operations/layer_helpers.test.ts | 29 +++++++ .../operations/layer_helpers.ts | 11 ++- x-pack/plugins/lens/public/types.ts | 5 +- 7 files changed, 135 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index bbc801ba01b7c..cab07150b6d56 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -332,7 +332,7 @@ describe('LayerPanel', () => { columns: {}, columnOrder: [], }, - true + { shouldReplaceDimension: true } ); }); expect(updateAll).toHaveBeenCalled(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 43c44dc3c07f5..831dfce2efadd 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -506,17 +506,32 @@ export function LayerPanel( columnId: activeId, filterOperations: activeGroup.filterOperations, dimensionGroups: groups, - setState: (newState: unknown, shouldUpdateVisualization?: boolean) => { - if (shouldUpdateVisualization) { + setState: ( + newState: unknown, + { + shouldReplaceDimension, + shouldRemoveDimension, + }: { + shouldReplaceDimension?: boolean; + shouldRemoveDimension?: boolean; + } = {} + ) => { + if (shouldReplaceDimension || shouldRemoveDimension) { props.updateAll( datasourceId, newState, - activeVisualization.setDimension({ - layerId, - groupId: activeGroup.groupId, - columnId: activeId, - prevState: props.visualizationState, - }) + shouldRemoveDimension + ? activeVisualization.removeDimension({ + layerId, + columnId: activeId, + prevState: props.visualizationState, + }) + : activeVisualization.setDimension({ + layerId, + groupId: activeGroup.groupId, + columnId: activeId, + prevState: props.visualizationState, + }) ); } else { props.updateDatasource(datasourceId, newState); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index fd62b8ca962ab..1bdffc90797ac 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -122,7 +122,14 @@ export function DimensionEditor(props: DimensionEditorProps) { const { fieldByOperation, operationWithoutField } = operationSupportMatrix; const setStateWrapper = (layer: IndexPatternLayer) => { - setState(mergeLayer({ state, layerId, newLayer: layer }), Boolean(layer.columns[columnId])); + const hasIncompleteColumns = Boolean(layer.incompleteColumns?.[columnId]); + const prevOperationType = + operationDefinitionMap[state.layers[layerId].columns[columnId]?.operationType]?.input; + setState(mergeLayer({ state, layerId, newLayer: layer }), { + shouldReplaceDimension: Boolean(layer.columns[columnId]), + // clear the dimension if there's an incomplete column pending && previous operation was a fullReference operation + shouldRemoveDimension: Boolean(hasIncompleteColumns && prevOperationType === 'fullReference'), + }); }; const selectedOperationDefinition = diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index fc6c317365886..f57c03f78ecaf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -492,7 +492,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); @@ -525,7 +525,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); @@ -559,7 +559,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); @@ -629,7 +629,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); @@ -667,7 +667,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); @@ -696,7 +696,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); @@ -816,7 +816,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - false + { shouldRemoveDimension: false, shouldReplaceDimension: false } ); const comboBox = wrapper @@ -847,7 +847,47 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } + ); + }); + + it('should clean up when transitioning from incomplete reference-based operations to field operation', () => { + wrapper = mount( + + ); + + // Transition to a field operation (incompatible) + wrapper + .find('button[data-test-subj="lns-indexPatternDimension-avg incompatible"]') + .simulate('click'); + + // Now check that the dimension gets cleaned up on state update + expect(setState).toHaveBeenCalledWith( + { + ...state, + layers: { + first: { + ...state.layers.first, + incompleteColumns: { + col2: { operationType: 'avg' }, + }, + }, + }, + }, + { shouldRemoveDimension: true, shouldReplaceDimension: false } ); }); @@ -945,7 +985,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); }); @@ -1037,7 +1077,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); @@ -1068,7 +1108,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); @@ -1097,7 +1137,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); @@ -1126,7 +1166,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); @@ -1155,7 +1195,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); @@ -1185,7 +1225,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); }); @@ -1238,7 +1278,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - false + { shouldRemoveDimension: false, shouldReplaceDimension: false } ); const comboBox = wrapper @@ -1268,7 +1308,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); @@ -1311,7 +1351,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); @@ -1337,7 +1377,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); @@ -1474,7 +1514,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - true + { shouldRemoveDimension: false, shouldReplaceDimension: true } ); }); @@ -1523,7 +1563,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, }, - false + { shouldRemoveDimension: false, shouldReplaceDimension: false } ); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index e0d9d864e5656..c0e71dc1509e7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -2174,5 +2174,34 @@ describe('state_helpers', () => { expect(mock).toHaveBeenCalled(); expect(errors).toHaveLength(1); }); + + it('should consider incompleteColumns before layer columns', () => { + const savedRef = jest.fn().mockReturnValue(['error 1']); + const incompleteRef = jest.fn(); + operationDefinitionMap.testReference.getErrorMessage = savedRef; + // @ts-expect-error invalid type, just need a single function on it + operationDefinitionMap.testIncompleteReference = { + getErrorMessage: incompleteRef, + }; + + const errors = getErrorMessages({ + indexPatternId: '1', + columnOrder: [], + columns: { + col1: + // @ts-expect-error not statically analyzed + { operationType: 'testReference', references: [] }, + }, + incompleteColumns: { + // @ts-expect-error not statically analyzed + col1: { operationType: 'testIncompleteReference' }, + }, + }); + expect(savedRef).not.toHaveBeenCalled(); + expect(incompleteRef).toHaveBeenCalled(); + expect(errors).toBeUndefined(); + + delete operationDefinitionMap.testIncompleteReference; + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 21fc36d7418ba..d8244f3902a6e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -316,6 +316,10 @@ export function replaceColumn({ } if (!field) { + // if no field is available perform a full clean of the column from the layer + if (previousDefinition.input === 'fullReference') { + tempLayer = deleteColumn({ layer: tempLayer, columnId, indexPattern }); + } return { ...tempLayer, incompleteColumns: { @@ -862,9 +866,12 @@ export function updateLayerIndexPattern( */ export function getErrorMessages(layer: IndexPatternLayer): string[] | undefined { const errors: string[] = []; - Object.entries(layer.columns).forEach(([columnId, column]) => { - const def = operationDefinitionMap[column.operationType]; + // If we're transitioning to another operation, check for "new" incompleteColumns rather + // than "old" saved operation on the layer + const columnFinalRef = + layer.incompleteColumns?.[columnId]?.operationType || column.operationType; + const def = operationDefinitionMap[columnFinalRef]; if (def.getErrorMessage) { errors.push(...(def.getErrorMessage(layer, columnId) ?? [])); } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 8a90e24a5dbe5..fe35e07081bf3 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -243,7 +243,10 @@ export type DatasourceDimensionProps = SharedDimensionProps & { // The only way a visualization has to restrict the query building export type DatasourceDimensionEditorProps = DatasourceDimensionProps & { // Not a StateSetter because we have this unique use case of determining valid columns - setState: (newState: Parameters>[0], publishToVisualization?: boolean) => void; + setState: ( + newState: Parameters>[0], + publishToVisualization?: { shouldReplaceDimension?: boolean; shouldRemoveDimension?: boolean } + ) => void; core: Pick; dateRange: DateRange; dimensionGroups: VisualizationDimensionGroupConfig[]; From b4820a7f79a32951c4672193e5045cecb59ed67c Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 18 Jan 2021 17:38:35 +0100 Subject: [PATCH 08/23] URL Drilldown global allow-list user docs (#88574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: ✏️ improve disalbe url drillown text * docs: ✏️ add external URL service docs to URL drilldown * docs: ✏️ typo * docs: update docs --- docs/user/dashboard/drilldowns.asciidoc | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc index 3db5bd6d97ff0..f7b55f17decf6 100644 --- a/docs/user/dashboard/drilldowns.asciidoc +++ b/docs/user/dashboard/drilldowns.asciidoc @@ -50,13 +50,29 @@ The <> you use to create a < Date: Mon, 18 Jan 2021 11:28:37 -0600 Subject: [PATCH 09/23] [Workplace Search] Add tests for Custom Source Display Settings (#88547) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add type Our code allows for an array but the type did not. * Add exampleResult mock * Add test-subj attrs * Remove FIXMEs for linter errors The linter was complaining when these were initially migrated, stating that a11y required all mouse events to have focus and blur events. This commit uses the hover events for those. EuiColorPicker was added in error and removing them does not disrupt the linter. * Add tests for components * Add test for router Also updates routes to use consistent syntax and remove the render prop * Use helper instead of history.push * Remove redundant resetDisplaySettingsState method Since all this does is wrap the clearFlashMessages function, we can just call it directly. Also use the new clearFlashMessages helper instead of using FlashMessageLogic directly insideof toggleFieldEditorModal * Add tests for DisplaySettings container * Fix a typo and associated tests Also adds ‘subtitleField’ that is needed in a future test * Add constants from PR to test https://github.com/elastic/kibana/pull/88477 * Add tests for DisplaySettingsLogic * Remove unused imports and use new mocks Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__mocks__/content_sources.mock.ts | 23 ++ .../applications/workplace_search/types.ts | 2 +- .../custom_source_icon.test.tsx | 18 + .../display_settings.test.tsx | 157 +++++++ .../display_settings/display_settings.tsx | 14 +- .../display_settings_logic.test.ts | 389 ++++++++++++++++++ .../display_settings_logic.ts | 11 +- .../display_settings_router.test.tsx | 29 ++ .../display_settings_router.tsx | 16 +- .../example_result_detail_card.test.tsx | 38 ++ .../example_result_detail_card.tsx | 12 +- .../example_search_result_group.test.tsx | 44 ++ .../example_search_result_group.tsx | 9 +- .../example_standout_result.test.tsx | 44 ++ .../example_standout_result.tsx | 9 +- .../field_editor_modal.test.tsx | 102 +++++ .../display_settings/result_detail.test.tsx | 131 ++++++ .../display_settings/result_detail.tsx | 8 +- .../display_settings/search_results.test.tsx | 124 ++++++ .../display_settings/search_results.tsx | 19 +- .../display_settings/subtitle_field.test.tsx | 35 ++ .../display_settings/subtitle_field.tsx | 5 +- .../display_settings/title_field.test.tsx | 46 +++ .../display_settings/title_field.tsx | 9 +- 24 files changed, 1243 insertions(+), 51 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index a3042f2df7ac7..edd69fa626b1d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -226,3 +226,26 @@ export const sourceConfigData = { consumerKey: 'elastic_enterprise_search_123', }, }; + +export const exampleResult = { + sourceName: 'source', + searchResultConfig: { + titleField: 'otherTitle', + subtitleField: 'otherSubtitle', + urlField: 'myLink', + color: '#e3e3e3', + descriptionField: 'about', + detailFields: [ + { fieldName: 'cats', label: 'Felines' }, + { fieldName: 'dogs', label: 'Canines' }, + ], + }, + titleFieldHover: false, + urlFieldHover: false, + exampleDocuments: [ + { + myLink: 'http://foo', + otherTitle: 'foo', + }, + ], +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index ed4946a019bb0..16ca141d91f47 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -181,7 +181,7 @@ export interface CustomSource { } export interface Result { - [key: string]: string; + [key: string]: string | string[]; } export interface OptionValue { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.test.tsx new file mode 100644 index 0000000000000..9d82ca9c1df19 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { CustomSourceIcon } from './custom_source_icon'; + +describe('CustomSourceIcon', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find('svg')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx new file mode 100644 index 0000000000000..73e40eec7774d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx @@ -0,0 +1,157 @@ +/* + * 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 '../../../../../__mocks__/shallow_useeffect.mock'; + +import { mockKibanaValues } from '../../../../../__mocks__'; + +import { setMockValues, setMockActions } from '../../../../../__mocks__'; +import { unmountHandler } from '../../../../../__mocks__/shallow_useeffect.mock'; + +import { shallow } from 'enzyme'; + +import React from 'react'; + +import { EuiButton, EuiTabbedContent } from '@elastic/eui'; + +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; + +import { Loading } from '../../../../../shared/loading'; +import { ViewContentHeader } from '../../../../components/shared/view_content_header'; + +import { FieldEditorModal } from './field_editor_modal'; + +import { DisplaySettings } from './display_settings'; + +describe('DisplaySettings', () => { + const { navigateToUrl } = mockKibanaValues; + const { exampleDocuments, searchResultConfig } = exampleResult; + const initializeDisplaySettings = jest.fn(); + const setServerData = jest.fn(); + const setColorField = jest.fn(); + + const values = { + isOrganization: true, + dataLoading: false, + sourceId: '123', + addFieldModalVisible: false, + unsavedChanges: false, + exampleDocuments, + searchResultConfig, + }; + + beforeEach(() => { + setMockActions({ + initializeDisplaySettings, + setServerData, + setColorField, + }); + setMockValues({ ...values }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find('form')).toHaveLength(1); + }); + + it('returns loading when loading', () => { + setMockValues({ ...values, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('handles window.onbeforeunload change', () => { + setMockValues({ ...values, unsavedChanges: true }); + shallow(); + + unmountHandler(); + + expect(window.onbeforeunload).toEqual(null); + }); + + it('handles window.onbeforeunload unmount', () => { + setMockValues({ ...values, unsavedChanges: true }); + shallow(); + + expect(window.onbeforeunload!({} as any)).toEqual( + 'Your display settings have not been saved. Are you sure you want to leave?' + ); + }); + + describe('tabbed content', () => { + const tabs = [ + { + id: 'search_results', + name: 'Search Results', + content: <>, + }, + { + id: 'result_detail', + name: 'Result Detail', + content: <>, + }, + ]; + + it('handles first tab click', () => { + const wrapper = shallow(); + const tabsEl = wrapper.find(EuiTabbedContent); + tabsEl.prop('onTabClick')!(tabs[0]); + + expect(navigateToUrl).toHaveBeenCalledWith('/sources/123/display_settings/'); + }); + + it('handles second tab click', () => { + const wrapper = shallow(); + const tabsEl = wrapper.find(EuiTabbedContent); + tabsEl.prop('onTabClick')!(tabs[1]); + + expect(navigateToUrl).toHaveBeenCalledWith('/sources/123/display_settings/result_detail'); + }); + }); + + describe('header action', () => { + it('renders button when hasDocuments', () => { + const wrapper = shallow(); + const button = ( + + Save + + ); + + expect(wrapper.find(ViewContentHeader).prop('action')).toStrictEqual(button); + }); + + it('renders null when no documents', () => { + setMockValues({ ...values, exampleDocuments: [] }); + const wrapper = shallow(); + + expect(wrapper.find(ViewContentHeader).prop('action')).toBeNull(); + }); + }); + + it('submits the form', () => { + const wrapper = shallow(); + const simulatedEvent = { + form: 0, + target: { getAttribute: () => '_self' }, + preventDefault: jest.fn(), + }; + + const form = wrapper.find('form'); + form.simulate('submit', simulatedEvent); + expect(simulatedEvent.preventDefault).toHaveBeenCalled(); + expect(setServerData).toHaveBeenCalled(); + }); + + it('renders FieldEditorModal', () => { + setMockValues({ ...values, addFieldModalVisible: true }); + const wrapper = shallow(); + + expect(wrapper.find(FieldEditorModal)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx index e34728beef5e5..cf066f3157e39 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx @@ -6,9 +6,7 @@ import React, { FormEvent, useEffect } from 'react'; -import { History } from 'history'; import { useActions, useValues } from 'kea'; -import { useHistory } from 'react-router-dom'; import './display_settings.scss'; @@ -26,6 +24,9 @@ import { getContentSourcePath, } from '../../../../routes'; +import { clearFlashMessages } from '../../../../../shared/flash_messages'; + +import { KibanaLogic } from '../../../../../shared/kibana'; import { AppLogic } from '../../../../app_logic'; import { Loading } from '../../../../../shared/loading'; @@ -45,10 +46,7 @@ interface DisplaySettingsProps { } export const DisplaySettings: React.FC = ({ tabId }) => { - const history = useHistory() as History; - const { initializeDisplaySettings, setServerData, resetDisplaySettingsState } = useActions( - DisplaySettingsLogic - ); + const { initializeDisplaySettings, setServerData } = useActions(DisplaySettingsLogic); const { dataLoading, @@ -64,7 +62,7 @@ export const DisplaySettings: React.FC = ({ tabId }) => { useEffect(() => { initializeDisplaySettings(); - return resetDisplaySettingsState; + return clearFlashMessages; }, []); useEffect(() => { @@ -95,7 +93,7 @@ export const DisplaySettings: React.FC = ({ tabId }) => { ? getContentSourcePath(DISPLAY_SETTINGS_RESULT_DETAIL_PATH, sourceId, isOrganization) : getContentSourcePath(DISPLAY_SETTINGS_SEARCH_RESULT_PATH, sourceId, isOrganization); - history.push(path); + KibanaLogic.values.navigateToUrl(path); }; const handleFormSubmit = (e: FormEvent) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts new file mode 100644 index 0000000000000..aed99bdd950c5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts @@ -0,0 +1,389 @@ +/* + * 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 { LogicMounter } from '../../../../../__mocks__/kea.mock'; + +import { + mockFlashMessageHelpers, + mockHttpValues, + expectedAsyncError, +} from '../../../../../__mocks__'; + +const contentSource = { id: 'source123' }; +jest.mock('../../source_logic', () => ({ + SourceLogic: { values: { contentSource } }, +})); + +import { AppLogic } from '../../../../app_logic'; +jest.mock('../../../../app_logic', () => ({ + AppLogic: { values: { isOrganization: true } }, +})); + +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; +import { LEAVE_UNASSIGNED_FIELD } from './constants'; + +import { DisplaySettingsLogic, defaultSearchResultConfig } from './display_settings_logic'; + +describe('DisplaySettingsLogic', () => { + const { http } = mockHttpValues; + const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + const { mount } = new LogicMounter(DisplaySettingsLogic); + + const { searchResultConfig, exampleDocuments } = exampleResult; + + const defaultValues = { + sourceName: '', + sourceId: '', + schemaFields: {}, + exampleDocuments: [], + serverSearchResultConfig: defaultSearchResultConfig, + searchResultConfig: defaultSearchResultConfig, + serverRoute: '', + editFieldIndex: null, + dataLoading: true, + addFieldModalVisible: false, + titleFieldHover: false, + urlFieldHover: false, + subtitleFieldHover: false, + descriptionFieldHover: false, + fieldOptions: [], + optionalFieldOptions: [ + { + value: LEAVE_UNASSIGNED_FIELD, + text: LEAVE_UNASSIGNED_FIELD, + }, + ], + availableFieldOptions: [], + unsavedChanges: false, + }; + + const serverProps = { + sourceName: 'foo', + sourceId: '123', + serverRoute: '/foo', + searchResultConfig, + exampleDocuments, + schemaFields: {}, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(DisplaySettingsLogic.values).toEqual(defaultValues); + }); + + describe('actions', () => { + it('onInitializeDisplaySettings', () => { + DisplaySettingsLogic.actions.onInitializeDisplaySettings(serverProps); + + expect(DisplaySettingsLogic.values.sourceName).toEqual(serverProps.sourceName); + expect(DisplaySettingsLogic.values.sourceId).toEqual(serverProps.sourceId); + expect(DisplaySettingsLogic.values.schemaFields).toEqual(serverProps.schemaFields); + expect(DisplaySettingsLogic.values.exampleDocuments).toEqual(serverProps.exampleDocuments); + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual( + serverProps.searchResultConfig + ); + expect(DisplaySettingsLogic.values.serverRoute).toEqual(serverProps.serverRoute); + expect(DisplaySettingsLogic.values.dataLoading).toEqual(false); + }); + + it('setServerResponseData', () => { + DisplaySettingsLogic.actions.setServerResponseData(serverProps); + + expect(DisplaySettingsLogic.values.serverSearchResultConfig).toEqual( + serverProps.searchResultConfig + ); + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual( + serverProps.searchResultConfig + ); + + expect(setSuccessMessage).toHaveBeenCalled(); + }); + + it('handles empty color', () => { + const propsWithoutColor = { + ...serverProps, + searchResultConfig: { + ...serverProps.searchResultConfig, + color: '', + }, + }; + const configWithDefaultColor = { + ...serverProps.searchResultConfig, + color: '#000000', + }; + DisplaySettingsLogic.actions.onInitializeDisplaySettings(propsWithoutColor); + + expect(DisplaySettingsLogic.values.serverSearchResultConfig).toEqual(configWithDefaultColor); + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual(configWithDefaultColor); + }); + + it('setTitleField', () => { + const TITLE = 'newTitle'; + DisplaySettingsLogic.actions.setServerResponseData(serverProps); + DisplaySettingsLogic.actions.setTitleField(TITLE); + + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual({ + ...searchResultConfig, + titleField: TITLE, + }); + }); + + it('setUrlField', () => { + const URL = 'http://new.url'; + DisplaySettingsLogic.actions.setServerResponseData(serverProps); + DisplaySettingsLogic.actions.setUrlField(URL); + + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual({ + ...searchResultConfig, + urlField: URL, + }); + }); + + it('setSubtitleField', () => { + const SUBTITLE = 'new sub title'; + DisplaySettingsLogic.actions.setServerResponseData(serverProps); + DisplaySettingsLogic.actions.setSubtitleField(SUBTITLE); + + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual({ + ...searchResultConfig, + subtitleField: SUBTITLE, + }); + }); + + it('setDescriptionField', () => { + const DESCRIPTION = 'new description'; + DisplaySettingsLogic.actions.setServerResponseData(serverProps); + DisplaySettingsLogic.actions.setDescriptionField(DESCRIPTION); + + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual({ + ...searchResultConfig, + descriptionField: DESCRIPTION, + }); + }); + + it('setColorField', () => { + const HEX = '#e3e3e3'; + DisplaySettingsLogic.actions.setServerResponseData(serverProps); + DisplaySettingsLogic.actions.setColorField(HEX); + + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual({ + ...searchResultConfig, + color: HEX, + }); + }); + + it('setDetailFields', () => { + const result = { + destination: { + index: 0, + }, + source: { + index: 1, + }, + }; + DisplaySettingsLogic.actions.setServerResponseData(serverProps); + DisplaySettingsLogic.actions.setDetailFields(result as any); + + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual({ + ...searchResultConfig, + detailFields: [searchResultConfig.detailFields[1], searchResultConfig.detailFields[0]], + }); + }); + + it('removeDetailField', () => { + DisplaySettingsLogic.actions.setServerResponseData(serverProps); + DisplaySettingsLogic.actions.removeDetailField(0); + + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual({ + ...searchResultConfig, + detailFields: [searchResultConfig.detailFields[1]], + }); + }); + + it('addDetailField', () => { + const newField = { label: 'Monkey', fieldName: 'primate' }; + DisplaySettingsLogic.actions.setServerResponseData(serverProps); + DisplaySettingsLogic.actions.addDetailField(newField); + + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual({ + ...searchResultConfig, + detailFields: [ + searchResultConfig.detailFields[0], + searchResultConfig.detailFields[1], + newField, + ], + }); + }); + + it('updateDetailField', () => { + const updatedField = { label: 'Monkey', fieldName: 'primate' }; + DisplaySettingsLogic.actions.setServerResponseData(serverProps); + DisplaySettingsLogic.actions.updateDetailField(updatedField, 0); + + expect(DisplaySettingsLogic.values.searchResultConfig).toEqual({ + ...searchResultConfig, + detailFields: [updatedField, searchResultConfig.detailFields[1]], + }); + }); + + it('openEditDetailField', () => { + const INDEX = 2; + DisplaySettingsLogic.actions.openEditDetailField(INDEX); + + expect(DisplaySettingsLogic.values.editFieldIndex).toEqual(INDEX); + }); + + it('toggleFieldEditorModal', () => { + DisplaySettingsLogic.actions.toggleFieldEditorModal(); + + expect(DisplaySettingsLogic.values.editFieldIndex).toEqual(null); + expect(DisplaySettingsLogic.values.addFieldModalVisible).toEqual( + !defaultValues.addFieldModalVisible + ); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + + it('toggleTitleFieldHover', () => { + DisplaySettingsLogic.actions.toggleTitleFieldHover(); + + expect(DisplaySettingsLogic.values.titleFieldHover).toEqual(!defaultValues.titleFieldHover); + }); + + it('toggleSubtitleFieldHover', () => { + DisplaySettingsLogic.actions.toggleSubtitleFieldHover(); + + expect(DisplaySettingsLogic.values.subtitleFieldHover).toEqual( + !defaultValues.subtitleFieldHover + ); + }); + + it('toggleDescriptionFieldHover', () => { + DisplaySettingsLogic.actions.toggleDescriptionFieldHover(); + + expect(DisplaySettingsLogic.values.descriptionFieldHover).toEqual( + !defaultValues.descriptionFieldHover + ); + }); + + it('toggleUrlFieldHover', () => { + DisplaySettingsLogic.actions.toggleUrlFieldHover(); + + expect(DisplaySettingsLogic.values.urlFieldHover).toEqual(!defaultValues.urlFieldHover); + }); + }); + + describe('listeners', () => { + describe('initializeDisplaySettings', () => { + it('calls API and sets values (org)', async () => { + const onInitializeDisplaySettingsSpy = jest.spyOn( + DisplaySettingsLogic.actions, + 'onInitializeDisplaySettings' + ); + const promise = Promise.resolve(serverProps); + http.get.mockReturnValue(promise); + DisplaySettingsLogic.actions.initializeDisplaySettings(); + + expect(http.get).toHaveBeenCalledWith( + '/api/workplace_search/org/sources/source123/display_settings/config' + ); + await promise; + expect(onInitializeDisplaySettingsSpy).toHaveBeenCalledWith({ + ...serverProps, + isOrganization: true, + }); + }); + + it('calls API and sets values (account)', async () => { + AppLogic.values.isOrganization = false; + + const onInitializeDisplaySettingsSpy = jest.spyOn( + DisplaySettingsLogic.actions, + 'onInitializeDisplaySettings' + ); + const promise = Promise.resolve(serverProps); + http.get.mockReturnValue(promise); + DisplaySettingsLogic.actions.initializeDisplaySettings(); + + expect(http.get).toHaveBeenCalledWith( + '/api/workplace_search/account/sources/source123/display_settings/config' + ); + await promise; + expect(onInitializeDisplaySettingsSpy).toHaveBeenCalledWith({ + ...serverProps, + isOrganization: false, + }); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + http.get.mockReturnValue(promise); + DisplaySettingsLogic.actions.initializeDisplaySettings(); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + describe('setServerData', () => { + it('calls API and sets values', async () => { + const setServerResponseDataSpy = jest.spyOn( + DisplaySettingsLogic.actions, + 'setServerResponseData' + ); + const promise = Promise.resolve(serverProps); + http.post.mockReturnValue(promise); + DisplaySettingsLogic.actions.onInitializeDisplaySettings(serverProps); + DisplaySettingsLogic.actions.setServerData(); + + expect(http.post).toHaveBeenCalledWith(serverProps.serverRoute, { + body: JSON.stringify({ ...searchResultConfig }), + }); + await promise; + expect(setServerResponseDataSpy).toHaveBeenCalledWith({ + ...serverProps, + }); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + http.post.mockReturnValue(promise); + DisplaySettingsLogic.actions.setServerData(); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + }); + + describe('selectors', () => { + describe('availableFieldOptions', () => { + it('should handle duplicate', () => { + const propsWithDuplicates = { + ...serverProps, + schemaFields: { + cats: 'string', + dogs: 'string', + monkeys: 'string', + }, + searchResultConfig: { + ...searchResultConfig, + detailsFields: [searchResultConfig.detailFields[0], searchResultConfig.detailFields[1]], + }, + }; + + DisplaySettingsLogic.actions.onInitializeDisplaySettings(propsWithDuplicates); + + expect(DisplaySettingsLogic.values.availableFieldOptions).toEqual([ + { text: 'monkeys', value: 'monkeys' }, + ]); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts index a8636b4a34da1..0e85e2ec57937 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts @@ -13,7 +13,7 @@ import { HttpLogic } from '../../../../../shared/http'; import { setSuccessMessage, - FlashMessagesLogic, + clearFlashMessages, flashAPIErrors, } from '../../../../../shared/flash_messages'; @@ -61,7 +61,6 @@ interface DisplaySettingsActions { toggleSubtitleFieldHover(): void; toggleDescriptionFieldHover(): void; toggleUrlFieldHover(): void; - resetDisplaySettingsState(): void; } interface DisplaySettingsValues { @@ -85,7 +84,7 @@ interface DisplaySettingsValues { unsavedChanges: boolean; } -const defaultSearchResultConfig = { +export const defaultSearchResultConfig = { titleField: '', subtitleField: '', descriptionField: '', @@ -117,7 +116,6 @@ export const DisplaySettingsLogic = kea< toggleSubtitleFieldHover: () => true, toggleDescriptionFieldHover: () => true, toggleUrlFieldHover: () => true, - resetDisplaySettingsState: () => true, initializeDisplaySettings: () => true, setServerData: () => true, }, @@ -330,10 +328,7 @@ export const DisplaySettingsLogic = kea< setSuccessMessage(SUCCESS_MESSAGE); }, toggleFieldEditorModal: () => { - FlashMessagesLogic.actions.clearFlashMessages(); - }, - resetDisplaySettingsState: () => { - FlashMessagesLogic.actions.clearFlashMessages(); + clearFlashMessages(); }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.test.tsx new file mode 100644 index 0000000000000..726bccb201c68 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.test.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues } from '../../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Route, Switch } from 'react-router-dom'; + +import { DisplaySettings } from './display_settings'; + +import { DisplaySettingsRouter } from './display_settings_router'; + +describe('DisplaySettingsRouter', () => { + it('renders', () => { + setMockValues({ isOrganization: true }); + const wrapper = shallow(); + + expect(wrapper.find(DisplaySettings)).toHaveLength(2); + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(2); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx index 01ac93735b8a8..2155d8358daed 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx @@ -23,16 +23,12 @@ export const DisplaySettingsRouter: React.FC = () => { const { isOrganization } = useValues(AppLogic); return ( - } - /> - } - /> + + + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.test.tsx new file mode 100644 index 0000000000000..30fcb0f6b1ac8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.test.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 '../../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues } from '../../../../../__mocks__'; +import { shallow } from 'enzyme'; + +import React from 'react'; + +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; + +import { ExampleResultDetailCard } from './example_result_detail_card'; + +describe('ExampleResultDetailCard', () => { + beforeEach(() => { + setMockValues({ ...exampleResult }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="ExampleResultDetailCard"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="DetailField"]')).toHaveLength( + exampleResult.searchResultConfig.detailFields.length + ); + }); + + it('shows fallback URL label when no override set', () => { + setMockValues({ ...exampleResult, searchResultConfig: { detailFields: [] } }); + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="DefaultUrlLabel"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx index 468f7d2f7ad05..3278140a2dfe6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx @@ -28,7 +28,7 @@ export const ExampleResultDetailCard: React.FC = () => { const result = exampleDocuments[0]; return ( -
+
@@ -49,7 +49,9 @@ export const ExampleResultDetailCard: React.FC = () => { {urlField ? (
{result[urlField]}
) : ( - URL + + URL + )}
@@ -57,7 +59,11 @@ export const ExampleResultDetailCard: React.FC = () => {
{detailFields.length > 0 ? ( detailFields.map(({ fieldName, label }, index) => ( -
+

{label}

diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx new file mode 100644 index 0000000000000..375e436b6a8a2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues } from '../../../../../__mocks__'; +import { shallow } from 'enzyme'; + +import React from 'react'; + +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; + +import { CustomSourceIcon } from './custom_source_icon'; + +import { ExampleSearchResultGroup } from './example_search_result_group'; + +describe('ExampleSearchResultGroup', () => { + beforeEach(() => { + setMockValues({ ...exampleResult }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="ExampleSearchResultGroup"]')).toHaveLength(1); + }); + + it('sets correct color prop when dark', () => { + setMockValues({ ...exampleResult, searchResultConfig: { color: '#000', detailFields: [] } }); + const wrapper = shallow(); + + expect(wrapper.find(CustomSourceIcon).prop('color')).toEqual('white'); + }); + + it('shows fallback URL label when no override set', () => { + setMockValues({ ...exampleResult, searchResultConfig: { detailFields: [], color: '#111' } }); + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="DefaultDescriptionLabel"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx index 14239b1654308..aa7bc4d917886 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx @@ -27,7 +27,7 @@ export const ExampleSearchResultGroup: React.FC = () => { } = useValues(DisplaySettingsLogic); return ( -
+
{ {descriptionField ? (
{result[descriptionField]}
) : ( - Description + + Description + )}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx new file mode 100644 index 0000000000000..4e56753bfa7e2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues } from '../../../../../__mocks__'; +import { shallow } from 'enzyme'; + +import React from 'react'; + +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; + +import { CustomSourceIcon } from './custom_source_icon'; + +import { ExampleStandoutResult } from './example_standout_result'; + +describe('ExampleStandoutResult', () => { + beforeEach(() => { + setMockValues({ ...exampleResult }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="ExampleStandoutResult"]')).toHaveLength(1); + }); + + it('sets correct color prop when dark', () => { + setMockValues({ ...exampleResult, searchResultConfig: { color: '#000', detailFields: [] } }); + const wrapper = shallow(); + + expect(wrapper.find(CustomSourceIcon).prop('color')).toEqual('white'); + }); + + it('shows fallback URL label when no override set', () => { + setMockValues({ ...exampleResult, searchResultConfig: { detailFields: [], color: '#111' } }); + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="DefaultDescriptionLabel"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx index 4ef3b1fe14b93..a80680d219aef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx @@ -30,7 +30,7 @@ export const ExampleStandoutResult: React.FC = () => { const result = exampleDocuments[0]; return ( -
+
{ {descriptionField ? ( {result[descriptionField]} ) : ( - Description + + Description + )}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.test.tsx new file mode 100644 index 0000000000000..5471df9a6f0be --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.test.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../../../__mocks__'; +import { shallow } from 'enzyme'; + +import React from 'react'; + +import { EuiModal, EuiSelect, EuiFieldText } from '@elastic/eui'; + +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; + +import { FieldEditorModal } from './field_editor_modal'; + +describe('FieldEditorModal', () => { + const { searchResultConfig } = exampleResult; + const fieldOptions = [ + { + value: 'foo', + text: 'Foo', + }, + ]; + const availableFieldOptions = [ + { + value: 'bar', + text: 'Bar', + }, + ]; + const toggleFieldEditorModal = jest.fn(); + const addDetailField = jest.fn(); + const updateDetailField = jest.fn(); + + beforeEach(() => { + setMockActions({ + toggleFieldEditorModal, + addDetailField, + updateDetailField, + }); + setMockValues({ + searchResultConfig, + fieldOptions, + availableFieldOptions, + editFieldIndex: 0, + }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiModal)).toHaveLength(1); + }); + + it('sets value on select change', () => { + const wrapper = shallow(); + const select = wrapper.find(EuiSelect); + + select.simulate('change', { target: { value: 'cats' } }); + + expect(select.prop('value')).toEqual('cats'); + }); + + it('sets value on input change', () => { + const wrapper = shallow(); + const input = wrapper.find(EuiFieldText); + + input.simulate('change', { target: { value: 'Felines' } }); + + expect(input.prop('value')).toEqual('Felines'); + }); + + it('handles form submission when creating', () => { + setMockValues({ + searchResultConfig, + fieldOptions, + availableFieldOptions, + editFieldIndex: null, + }); + + const wrapper = shallow(); + + const preventDefault = jest.fn(); + wrapper.find('form').simulate('submit', { preventDefault }); + + expect(preventDefault).toHaveBeenCalled(); + expect(addDetailField).toHaveBeenCalled(); + }); + + it('handles form submission when editing', () => { + const wrapper = shallow(); + + const preventDefault = jest.fn(); + wrapper.find('form').simulate('submit', { preventDefault }); + + expect(preventDefault).toHaveBeenCalled(); + expect(updateDetailField).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.test.tsx new file mode 100644 index 0000000000000..6d9d60351f3db --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.test.tsx @@ -0,0 +1,131 @@ +/* + * 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 '../../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../../../__mocks__'; +import { shallow, mount } from 'enzyme'; + +/** + * Mocking necessary due to console warnings from react d-n-d, which EUI uses + * https://stackoverflow.com/a/56674119/1949235 + */ +jest.mock('react-beautiful-dnd', () => ({ + Droppable: ({ children }: { children: any }) => + children( + { + draggableProps: { + style: {}, + }, + innerRef: jest.fn(), + }, + {} + ), + Draggable: ({ children }: { children: any }) => + children( + { + draggableProps: { + style: {}, + }, + innerRef: jest.fn(), + }, + {} + ), + DragDropContext: ({ children }: { children: any }) => children, +})); + +import React from 'react'; + +import { EuiTextColor } from '@elastic/eui'; + +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; + +import { ExampleResultDetailCard } from './example_result_detail_card'; + +import { ResultDetail } from './result_detail'; + +describe('ResultDetail', () => { + const { searchResultConfig, exampleDocuments } = exampleResult; + const availableFieldOptions = [ + { + value: 'foo', + text: 'Foo', + }, + ]; + const toggleFieldEditorModal = jest.fn(); + const setDetailFields = jest.fn(); + const openEditDetailField = jest.fn(); + const removeDetailField = jest.fn(); + + beforeEach(() => { + setMockActions({ + toggleFieldEditorModal, + setDetailFields, + openEditDetailField, + removeDetailField, + }); + setMockValues({ + searchResultConfig, + availableFieldOptions, + exampleDocuments, + }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(ExampleResultDetailCard)).toHaveLength(1); + }); + + it('calls setTitleField on change', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="AddFieldButton"]').simulate('click'); + + expect(toggleFieldEditorModal).toHaveBeenCalled(); + }); + + it('handles empty detailFields', () => { + setMockValues({ + searchResultConfig: { + ...searchResultConfig, + detailFields: [], + }, + availableFieldOptions, + exampleDocuments, + }); + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="EmptyFieldsDescription"]')).toHaveLength(1); + }); + + it('handles drag and drop', () => { + const wrapper = mount(); + wrapper.find('[data-test-subj="EditFieldButton"]').first().simulate('click'); + wrapper.find('[data-test-subj="RemoveFieldButton"]').first().simulate('click'); + + expect(openEditDetailField).toHaveBeenCalled(); + expect(removeDetailField).toHaveBeenCalled(); + expect(wrapper.find(EuiTextColor).first().text()).toEqual('“Felines”'); + }); + + it('handles empty label fallback', () => { + setMockValues({ + searchResultConfig: { + ...searchResultConfig, + detailFields: [ + { + fieldName: 'foo', + }, + ], + }, + availableFieldOptions, + exampleDocuments, + }); + const wrapper = mount(); + + expect(wrapper.find(EuiTextColor).first().text()).toEqual('“”'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx index cb65d8ef671e6..5ee484250ca62 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx @@ -80,7 +80,7 @@ export const ResultDetail: React.FC = () => { <> {detailFields.map(({ fieldName, label }, index) => ( {
openEditDetailField(index)} /> removeDetailField(index)} @@ -125,7 +127,9 @@ export const ResultDetail: React.FC = () => { ) : ( -

Add fields and move them into the order you want them to appear.

+

+ Add fields and move them into the order you want them to appear. +

)} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx new file mode 100644 index 0000000000000..776bf012aa895 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../../../__mocks__'; +import { shallow } from 'enzyme'; + +import React from 'react'; + +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; + +import { ExampleSearchResultGroup } from './example_search_result_group'; +import { ExampleStandoutResult } from './example_standout_result'; + +import { LEAVE_UNASSIGNED_FIELD } from './constants'; +import { SearchResults } from './search_results'; + +describe('SearchResults', () => { + const { searchResultConfig } = exampleResult; + const fieldOptions = [ + { + value: 'foo', + text: 'Foo', + }, + ]; + const optionalFieldOptions = [ + { + value: 'bar', + text: 'Bar', + }, + ]; + const toggleTitleFieldHover = jest.fn(); + const toggleSubtitleFieldHover = jest.fn(); + const toggleDescriptionFieldHover = jest.fn(); + const setTitleField = jest.fn(); + const setSubtitleField = jest.fn(); + const setDescriptionField = jest.fn(); + const setUrlField = jest.fn(); + const setColorField = jest.fn(); + + beforeEach(() => { + setMockActions({ + toggleTitleFieldHover, + toggleSubtitleFieldHover, + toggleDescriptionFieldHover, + setTitleField, + setSubtitleField, + setDescriptionField, + setUrlField, + setColorField, + }); + setMockValues({ + searchResultConfig, + fieldOptions, + optionalFieldOptions, + }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(ExampleSearchResultGroup)).toHaveLength(1); + expect(wrapper.find(ExampleStandoutResult)).toHaveLength(1); + }); + + it('calls setTitleField on change', () => { + const wrapper = shallow(); + wrapper + .find('[data-test-subj="TitleFieldSelect"]') + .simulate('change', { target: { value: searchResultConfig.titleField } }); + + expect(setTitleField).toHaveBeenCalled(); + }); + + it('calls setUrlField on change', () => { + const wrapper = shallow(); + wrapper + .find('[data-test-subj="UrlFieldSelect"]') + .simulate('change', { target: { value: searchResultConfig.urlField } }); + + expect(setUrlField).toHaveBeenCalled(); + }); + + it('calls setSubtitleField on change', () => { + const wrapper = shallow(); + wrapper + .find('[data-test-subj="SubtitleFieldSelect"]') + .simulate('change', { target: { value: searchResultConfig.titleField } }); + + expect(setSubtitleField).toHaveBeenCalledWith(searchResultConfig.titleField); + }); + + it('calls setDescriptionField on change', () => { + const wrapper = shallow(); + wrapper + .find('[data-test-subj="DescriptionFieldSelect"]') + .simulate('change', { target: { value: searchResultConfig.descriptionField } }); + + expect(setDescriptionField).toHaveBeenCalledWith(searchResultConfig.descriptionField); + }); + + it('handles blank fallbacks', () => { + setMockValues({ + searchResultConfig: { detailFields: [] }, + fieldOptions, + optionalFieldOptions, + }); + const wrapper = shallow(); + wrapper + .find('[data-test-subj="SubtitleFieldSelect"]') + .simulate('change', { target: { value: LEAVE_UNASSIGNED_FIELD } }); + wrapper + .find('[data-test-subj="DescriptionFieldSelect"]') + .simulate('change', { target: { value: LEAVE_UNASSIGNED_FIELD } }); + + expect(wrapper.find('[data-test-subj="UrlFieldSelect"]').prop('value')).toEqual(''); + expect(setSubtitleField).toHaveBeenCalledWith(null); + expect(setDescriptionField).toHaveBeenCalledWith(null); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx index 96b7a6fbe14b5..c1a65d1c52b65 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx @@ -59,8 +59,8 @@ export const SearchResults: React.FC = () => { label="Title" onMouseOver={toggleTitleFieldHover} onMouseOut={toggleTitleFieldHover} - onFocus={() => null} // FIXME - onBlur={() => null} // FIXME + onFocus={toggleTitleFieldHover} + onBlur={toggleTitleFieldHover} > { /> - null} // FIXME - onBlur={() => null} // FIXME - /> + null} // FIXME - onBlur={() => null} // FIXME + onFocus={toggleSubtitleFieldHover} + onBlur={toggleSubtitleFieldHover} > { helpText="This area is optional" onMouseOver={toggleDescriptionFieldHover} onMouseOut={toggleDescriptionFieldHover} - onFocus={() => null} // FIXME - onBlur={() => null} // FIXME + onFocus={toggleDescriptionFieldHover} + onBlur={toggleDescriptionFieldHover} > { + const result = { foo: 'bar' }; + it('renders', () => { + const props = { + result, + subtitleField: 'foo', + subtitleFieldHover: false, + }; + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="SubtitleField"]')).toHaveLength(1); + }); + + it('shows fallback URL label when no override set', () => { + const props = { + result, + subtitleField: null, + subtitleFieldHover: false, + }; + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="DefaultSubtitleLabel"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx index e27052ddffc04..d2f26cd6726df 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.tsx @@ -22,6 +22,7 @@ export const SubtitleField: React.FC = ({ subtitleFieldHover, }) => (
= ({ {subtitleField ? (
{result[subtitleField]}
) : ( - Subtitle + + Subtitle + )}
); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx new file mode 100644 index 0000000000000..558fd7aa8c86a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { TitleField } from './title_field'; + +describe('TitleField', () => { + const result = { foo: 'bar' }; + it('renders', () => { + const props = { + result, + titleField: 'foo', + titleFieldHover: false, + }; + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="TitleField"]')).toHaveLength(1); + }); + + it('handles title when array', () => { + const props = { + result: { foo: ['baz', 'bar'] }, + titleField: 'foo', + titleFieldHover: false, + }; + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="CustomTitleLabel"]').text()).toEqual('baz, bar'); + }); + + it('shows fallback URL label when no override set', () => { + const props = { + result, + titleField: null, + titleFieldHover: false, + }; + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="DefaultTitleLabel"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx index a54c0977b464f..fa975c8b11ce0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.tsx @@ -21,14 +21,19 @@ export const TitleField: React.FC = ({ result, titleField, titl const titleDisplay = Array.isArray(title) ? title.join(', ') : title; return (
{titleField ? ( -
{titleDisplay}
+
+ {titleDisplay} +
) : ( - Title + + Title + )}
); From 32bd5ce13c67e36078821f3e6fa5b0ef448377d7 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 18 Jan 2021 17:40:47 +0000 Subject: [PATCH 10/23] chore(NA): add a supplementary group on kibana docker config to match ES configs (#88606) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../tasks/os_packages/docker_generator/templates/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile index 9c78821f331e4..1a0e325a2486a 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile @@ -97,7 +97,7 @@ RUN find / -xdev -perm -4000 -exec chmod u-s {} + # Provide a non-root user to run the process. RUN groupadd --gid 1000 kibana && \ - useradd --uid 1000 --gid 1000 \ + useradd --uid 1000 --gid 1000 -G 0 \ --home-dir /usr/share/kibana --no-create-home \ kibana From af7a577b09f1db8e9c2cd7aa04999bd2417df9ab Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 18 Jan 2021 13:51:32 -0500 Subject: [PATCH 11/23] [Time to Visualize] Enable by Default (#88390) * Enable Time to Visualize by Default --- src/plugins/dashboard/config.ts | 2 +- .../saved_object_save_modal_dashboard.tsx | 8 +-- .../utils/use/use_saved_vis_instance.ts | 3 +- .../functional/page_objects/visualize_page.ts | 66 +++++++++++++++---- .../test/functional/page_objects/lens_page.ts | 28 ++++---- .../functional/tests/visualize_integration.ts | 5 +- 6 files changed, 76 insertions(+), 36 deletions(-) diff --git a/src/plugins/dashboard/config.ts b/src/plugins/dashboard/config.ts index ff968a51679e0..da3f8a61306b8 100644 --- a/src/plugins/dashboard/config.ts +++ b/src/plugins/dashboard/config.ts @@ -20,7 +20,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const configSchema = schema.object({ - allowByValueEmbeddables: schema.boolean({ defaultValue: false }), + allowByValueEmbeddables: schema.boolean({ defaultValue: true }), }); export type ConfigSchema = TypeOf; diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx index aafa500002ae2..04700e9379840 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx @@ -99,11 +99,11 @@ export function SavedObjectSaveModalDashboard(props: DashboardSaveModalProps) { } hasChildLabel={false} > - +
navigateToApp(originatingApp) : undefined; - const byValueCreateMode = dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables; + const byValueCreateMode = + Boolean(originatingApp) && dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables; if (savedVis.id) { chrome.setBreadcrumbs( diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index d8329f492fe80..d5df15968f741 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -20,6 +20,18 @@ import { FtrProviderContext } from '../ftr_provider_context'; import { VisualizeConstants } from '../../../src/plugins/visualize/public/application/visualize_constants'; +interface VisualizeSaveModalArgs { + saveAsNew?: boolean; + redirectToOrigin?: boolean; + addToDashboard?: boolean; + dashboardId?: string; +} + +type DashboardPickerOption = + | 'add-to-library-option' + | 'existing-dashboard-option' + | 'new-dashboard-option'; + export function VisualizePageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); @@ -346,11 +358,27 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide } } - public async saveVisualization( + public async saveVisualization(vizName: string, saveModalArgs: VisualizeSaveModalArgs = {}) { + await this.ensureSavePanelOpen(); + + await this.setSaveModalValues(vizName, saveModalArgs); + log.debug('Click Save Visualization button'); + + await testSubjects.click('confirmSaveSavedObjectButton'); + + // Confirm that the Visualization has actually been saved + await testSubjects.existOrFail('saveVisualizationSuccess'); + const message = await common.closeToast(); + await header.waitUntilLoadingHasFinished(); + await common.waitForSaveModalToClose(); + + return message; + } + + public async setSaveModalValues( vizName: string, - { saveAsNew = false, redirectToOrigin = false } = {} + { saveAsNew, redirectToOrigin, addToDashboard, dashboardId }: VisualizeSaveModalArgs = {} ) { - await this.ensureSavePanelOpen(); await testSubjects.setValue('savedObjectTitle', vizName); const saveAsNewCheckboxExists = await testSubjects.exists('saveAsNewCheckbox'); @@ -366,24 +394,34 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide log.debug('redirect to origin checkbox exists. Setting its state to', state); await testSubjects.setEuiSwitch('returnToOriginModeSwitch', state); } - log.debug('Click Save Visualization button'); - await testSubjects.click('confirmSaveSavedObjectButton'); - - // Confirm that the Visualization has actually been saved - await testSubjects.existOrFail('saveVisualizationSuccess'); - const message = await common.closeToast(); - await header.waitUntilLoadingHasFinished(); - await common.waitForSaveModalToClose(); + const dashboardSelectorExists = await testSubjects.exists('add-to-dashboard-options'); + if (dashboardSelectorExists) { + let option: DashboardPickerOption = 'add-to-library-option'; + if (addToDashboard) { + option = dashboardId ? 'existing-dashboard-option' : 'new-dashboard-option'; + } + log.debug('save modal dashboard selector, choosing option:', option); + const dashboardSelector = await testSubjects.find('add-to-dashboard-options'); + const label = await dashboardSelector.findByCssSelector(`label[for="${option}"]`); + await label.click(); - return message; + if (dashboardId) { + // TODO - selecting an existing dashboard + } + } } public async saveVisualizationExpectSuccess( vizName: string, - { saveAsNew = false, redirectToOrigin = false } = {} + { saveAsNew, redirectToOrigin, addToDashboard, dashboardId }: VisualizeSaveModalArgs = {} ) { - const saveMessage = await this.saveVisualization(vizName, { saveAsNew, redirectToOrigin }); + const saveMessage = await this.saveVisualization(vizName, { + saveAsNew, + redirectToOrigin, + addToDashboard, + dashboardId, + }); if (!saveMessage) { throw new Error( `Expected saveVisualization to respond with the saveMessage from the toast, got ${saveMessage}` diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 13ff6a64f8936..04c660847bcee 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -16,7 +16,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont const find = getService('find'); const comboBox = getService('comboBox'); const browser = getService('browser'); - const PageObjects = getPageObjects(['header', 'timePicker', 'common']); + const PageObjects = getPageObjects(['header', 'timePicker', 'common', 'visualize']); return logWrapper('lensPage', log, { /** @@ -270,22 +270,22 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont /** * Save the current Lens visualization. */ - async save(title: string, saveAsNew?: boolean, redirectToOrigin?: boolean) { + async save( + title: string, + saveAsNew?: boolean, + redirectToOrigin?: boolean, + addToDashboard?: boolean, + dashboardId?: string + ) { await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.click('lnsApp_saveButton'); - await testSubjects.setValue('savedObjectTitle', title); - - const saveAsNewCheckboxExists = await testSubjects.exists('saveAsNewCheckbox'); - if (saveAsNewCheckboxExists) { - const state = saveAsNew ? 'check' : 'uncheck'; - await testSubjects.setEuiSwitch('saveAsNewCheckbox', state); - } - const redirectToOriginCheckboxExists = await testSubjects.exists('returnToOriginModeSwitch'); - if (redirectToOriginCheckboxExists) { - const state = redirectToOrigin ? 'check' : 'uncheck'; - await testSubjects.setEuiSwitch('returnToOriginModeSwitch', state); - } + await PageObjects.visualize.setSaveModalValues(title, { + saveAsNew, + redirectToOrigin, + addToDashboard, + dashboardId, + }); await testSubjects.click('confirmSaveSavedObjectButton'); await retry.waitForWithTimeout('Save modal to disappear', 1000, () => diff --git a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts index 834c3083071df..e92ba226f3959 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts @@ -94,7 +94,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); await PageObjects.visualize.ensureSavePanelOpen(); - await testSubjects.setValue('savedObjectTitle', 'My new markdown viz'); + await PageObjects.visualize.setSaveModalValues('My new markdown viz'); + await selectSavedObjectTags('tag-1'); await testSubjects.click('confirmSaveSavedObjectButton'); @@ -118,7 +119,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); await PageObjects.visualize.ensureSavePanelOpen(); - await testSubjects.setValue('savedObjectTitle', 'vis-with-new-tag'); + await PageObjects.visualize.setSaveModalValues('vis-with-new-tag'); await testSubjects.click('savedObjectTagSelector'); await testSubjects.click(`tagSelectorOption-action__create`); From 89dc4f2e007afda638a51af7ccc4ee24cf4c21a5 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 18 Jan 2021 21:05:29 +0100 Subject: [PATCH 12/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20enable=20URL=20dri?= =?UTF-8?q?lldown=20test=20(#88580)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../url_drilldown_collect_config.tsx | 2 ++ .../apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx index 04ed73b2ce0b8..e381a55397c3b 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx @@ -121,6 +121,7 @@ export const UrlDrilldownCollectConfig: React.FC = ({ @@ -131,6 +132,7 @@ export const UrlDrilldownCollectConfig: React.FC = ({ label={txtUrlTemplateOpenInNewTab} checked={config.openInNewTab} onChange={() => onConfig({ ...config, openInNewTab: !config.openInNewTab })} + data-test-subj="urlDrilldownOpenInNewTab" /> diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts index d44a373f43040..fe2d947cb6420 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts @@ -25,7 +25,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.preserveCrossAppState(); }); - it.skip('should create dashboard to URL drilldown and use it to navigate to discover', async () => { + it('should create dashboard to URL drilldown and use it to navigate to discover', async () => { await PageObjects.dashboard.gotoDashboardEditMode( dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME ); @@ -43,6 +43,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { destinationURLTemplate: urlTemplate, trigger: 'SELECT_RANGE_TRIGGER', }); + + await testSubjects.click('urlDrilldownAdditionalOptions'); + await testSubjects.click('urlDrilldownOpenInNewTab'); + await dashboardDrilldownsManage.saveChanges(); await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); From 5f0e9f424838b38f5f432c04e37c53c02d3903c1 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Mon, 18 Jan 2021 14:14:42 -0600 Subject: [PATCH 13/23] [Workplace Search] Migrates Org Settings tree (#88092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial copy/paste of component tree Only does linting changes and lodash import * Replace withRouter HOC with hooks Use useLocation and no longer pass around history, using the KibanaLogic navigateToUrl method instead * Migrate LicenseCallout component * Update paths Also change name of component to OauthApplication as the default import was named that before * Remove conditional and passed in flash messages This is no longer needed with the Kibana syntax. Flash messages are set globally and only render when present. * Replace removed ConfirmModal In Kibana, we use the Eui components directly * Use internal tools for determining license * Fix a bunch of type issues * Remove telemetry settings section We no longer control telemetry in Workplace Search. It is handled by Kibana itself * Add SettingsSubNav component * Add route and update nav * Remove legacy AppView and sidenav * Clear flash messages globally * Remove global name change method call The global org name is not used anywhere outside of this section so we no longer need to update the global organization object as it is re-fetched when this section is entered. Previously, we displayed the org name in the sidebar but that has been removed in Kibana * Refactor saveUpdatedConfig We recently split out the logic from SourceLogic to the new AddSourceLogic and in settings, we were calling the saveSourceConfig method from the SourceLogic (now in AddSourceLogic) file and passing a callback that set the flash messages from that component’s state. Now, we set the flash messages globally and no longer need to have a saveUpdatedConfig method in the logic file, but can call it directly in the component and the flash messages will be set globally. * Update logic file to use global flash messages * Update server routes * Replace Rails http with kibana http * Fix subnav * Update routes to use consistent syntax We use this method across both Enterprise Search products in Kibana * Shorten nav item copy This would be the only place in the sidebar with a nav item breaking to a second line. * Fix some random typos * Replace React Router Link with helper * Add i18n * Remove redundant clearing of flash messages This happens automatically now in the global flash messages logic; route changes trigger clearing of messages * Add unit tests for components * Add tests for router * Store oauthApplication in mock for reuse * Add tests for SettingsLogic * Fix typo * Remove unncessary imports Copied from this PR: https://github.com/elastic/kibana/pull/88525 * Refactor to use new helpers when mocking See https://github.com/elastic/kibana/pull/88494 * Update logic test to use error helper See https://github.com/elastic/kibana/pull/88422 * Fix type issue * Fix whitespace lint issue Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__mocks__/content_sources.mock.ts | 9 + .../components/layout/nav.tsx | 9 +- .../shared/license_callout/index.ts | 7 + .../license_callout/license_callout.test.tsx | 21 ++ .../license_callout/license_callout.tsx | 46 ++++ .../workplace_search/constants.ts | 224 ++++++++++++++++- .../applications/workplace_search/index.tsx | 19 +- .../settings/components/connectors.test.tsx | 53 ++++ .../views/settings/components/connectors.tsx | 141 +++++++++++ .../settings/components/customize.test.tsx | 50 ++++ .../views/settings/components/customize.tsx | 63 +++++ .../components/oauth_application.test.tsx | 133 ++++++++++ .../settings/components/oauth_application.tsx | 203 +++++++++++++++ .../components/settings_sub_nav.test.tsx | 20 ++ .../settings/components/settings_sub_nav.tsx | 27 ++ .../components/source_config.test.tsx | 87 +++++++ .../settings/components/source_config.tsx | 80 ++++++ .../workplace_search/views/settings/index.ts | 7 + .../views/settings/settings_logic.test.ts | 234 ++++++++++++++++++ .../views/settings/settings_logic.ts | 197 +++++++++++++++ .../views/settings/settings_router.test.tsx | 49 ++++ .../views/settings/settings_router.tsx | 59 +++++ 22 files changed, 1729 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index edd69fa626b1d..b3962a7c88cc8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -227,6 +227,15 @@ export const sourceConfigData = { }, }; +export const oauthApplication = { + name: 'app', + uid: '123uid123', + secret: 'shhhhhhhhh', + redirectUri: 'https://foo', + confidential: false, + nativeRedirectUri: 'https://bar', +}; + export const exampleResult = { sourceName: 'source', searchResultConfig: { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx index 944820c4b1c40..8a83e9aad5fd9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx @@ -24,9 +24,14 @@ import { interface Props { sourcesSubNav?: React.ReactNode; groupsSubNav?: React.ReactNode; + settingsSubNav?: React.ReactNode; } -export const WorkplaceSearchNav: React.FC = ({ sourcesSubNav, groupsSubNav }) => ( +export const WorkplaceSearchNav: React.FC = ({ + sourcesSubNav, + groupsSubNav, + settingsSubNav, +}) => ( {NAV.OVERVIEW} @@ -43,7 +48,7 @@ export const WorkplaceSearchNav: React.FC = ({ sourcesSubNav, groupsSubNa {NAV.SECURITY} - + {NAV.SETTINGS} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/index.ts new file mode 100644 index 0000000000000..706dc1d75d9ce --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { LicenseCallout } from './license_callout'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.test.tsx new file mode 100644 index 0000000000000..b3bbd850be50a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiLink, EuiText } from '@elastic/eui'; + +import { LicenseCallout } from '.'; + +describe('LicenseCallout', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiLink)).toHaveLength(1); + expect(wrapper.find(EuiText)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.tsx new file mode 100644 index 0000000000000..166925c502329 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiLink, EuiFlexItem, EuiFlexGroup, EuiText } from '@elastic/eui'; + +import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes'; + +interface LicenseCalloutProps { + message?: string; +} + +export const LicenseCallout: React.FC = ({ message }) => { + const title = ( + <> + {message}{' '} + + Explore Platinum features + + + ); + + return ( +
+ + +
+ +
+
+ + {title} + +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index 4ca256ac91a3f..6eedc9270b83f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -43,6 +43,21 @@ export const NAV = { SETTINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.settings', { defaultMessage: 'Settings', }), + SETTINGS_CUSTOMIZE: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.nav.settingsCustomize', + { + defaultMessage: 'Customize', + } + ), + SETTINGS_SOURCE_PRIORITIZATION: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.nav.settingsSourcePrioritization', + { + defaultMessage: 'Content source connectors', + } + ), + SETTINGS_OAUTH: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.settingsOauth', { + defaultMessage: 'OAuth application', + }), ADD_SOURCE: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.addSource', { defaultMessage: 'Add Source', }), @@ -275,43 +290,240 @@ export const DOCUMENTATION_LINK_TITLE = i18n.translate( ); export const PUBLIC_KEY_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearc.publicKey.label', + 'xpack.enterpriseSearch.workplaceSearch.publicKey.label', { defaultMessage: 'Public Key', } ); export const CONSUMER_KEY_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearc.consumerKey.label', + 'xpack.enterpriseSearch.workplaceSearch.consumerKey.label', { defaultMessage: 'Consumer Key', } ); export const BASE_URI_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearc.baseUri.label', + 'xpack.enterpriseSearch.workplaceSearch.baseUri.label', { defaultMessage: 'Base URI', } ); export const BASE_URL_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearc.baseUrl.label', + 'xpack.enterpriseSearch.workplaceSearch.baseUrl.label', { defaultMessage: 'Base URL', } ); export const CLIENT_ID_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearc.clientId.label', + 'xpack.enterpriseSearch.workplaceSearch.clientId.label', { defaultMessage: 'Client id', } ); export const CLIENT_SECRET_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearc.clientSecret.label', + 'xpack.enterpriseSearch.workplaceSearch.clientSecret.label', { defaultMessage: 'Client secret', } ); + +export const CONFIDENTIAL_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.confidential.label', + { + defaultMessage: 'Confidential', + } +); + +export const CONFIDENTIAL_HELP_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.confidential.text', + { + defaultMessage: + 'Deselect for environments in which the client secret cannot be kept confidential, such as native mobile apps and single page applications.', + } +); + +export const CREDENTIALS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.credentials.title', + { + defaultMessage: 'Credentials', + } +); + +export const CREDENTIALS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.credentials.description', + { + defaultMessage: + 'Use the following credentials within your client to request access tokens from our authentication server.', + } +); + +export const ORG_UPDATED_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.orgUpdated.message', + { + defaultMessage: 'Successfully updated organization.', + } +); + +export const OAUTH_APP_UPDATED_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.oauthAppUpdated.message', + { + defaultMessage: 'Successfully updated application.', + } +); + +export const SAVE_CHANGES_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.saveChanges.button', + { + defaultMessage: 'Save changes', + } +); + +export const NAME_LABEL = i18n.translate('xpack.enterpriseSearch.workplaceSearch.name.label', { + defaultMessage: 'Name', +}); + +export const OAUTH_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.oauth.description', + { + defaultMessage: 'Create an OAuth client for your organization.', + } +); + +export const OAUTH_PERSISTED_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.oauthPersisted.description', + { + defaultMessage: + "Access your organization's OAuth client credentials and manage OAuth settings.", + } +); + +export const REDIRECT_URIS_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.redirectURIs.label', + { + defaultMessage: 'Redirect URIs', + } +); + +export const REDIRECT_HELP_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.redirectHelp.text', + { + defaultMessage: 'Provide one URI per line.', + } +); + +export const REDIRECT_NATIVE_HELP_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.redirectNativeHelp.text', + { + defaultMessage: 'For local development URIs, use format', + } +); + +export const REDIRECT_SECURE_ERROR_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.redirectSecureError.text', + { + defaultMessage: 'Cannot contain duplicate redirect URIs.', + } +); + +export const REDIRECT_INSECURE_ERROR_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.redirectInsecureError.text', + { + defaultMessage: 'Using an insecure redirect URI (http) is not recommended.', + } +); + +export const LICENSE_MODAL_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.licenseModal.title', + { + defaultMessage: 'Configuring OAuth for Custom Search Applications', + } +); + +export const LICENSE_MODAL_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.licenseModal.description', + { + defaultMessage: + 'Configure an OAuth application for secure use of the Workplace Search Search API. Upgrade to a Platinum license to enable the Search API and create your OAuth application.', + } +); + +export const LICENSE_MODAL_LINK = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.licenseModal.link', + { + defaultMessage: 'Explore Platinum features', + } +); + +export const CUSTOMIZE_HEADER_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.customize.header.title', + { + defaultMessage: 'Customize Workplace Search', + } +); + +export const CUSTOMIZE_HEADER_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.customize.header.description', + { + defaultMessage: 'Personalize general organization settings.', + } +); + +export const CUSTOMIZE_NAME_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.customize.name.label', + { + defaultMessage: 'Personalize general organization settings.', + } +); + +export const CUSTOMIZE_NAME_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.customize.name.button', + { + defaultMessage: 'Save organization name', + } +); + +export const UPDATE_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.update.button', + { + defaultMessage: 'Update', + } +); + +export const CONFIGURE_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.configure.button', + { + defaultMessage: 'Configure', + } +); + +export const PRIVATE_PLATINUM_LICENSE_CALLOUT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.privatePlatinumCallout.text', + { + defaultMessage: 'Private Sources require a Platinum license.', + } +); + +export const PRIVATE_SOURCE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.privateSource.text', + { + defaultMessage: 'Private Source', + } +); + +export const CONNECTORS_HEADER_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.connectors.header.title', + { + defaultMessage: 'Content source connectors', + } +); + +export const CONNECTORS_HEADER_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.connectors.header.description', + { + defaultMessage: 'All of your configurable connectors.', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 562a2ffb32888..65a2c7a4a44dd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -16,7 +16,13 @@ import { AppLogic } from './app_logic'; import { Layout } from '../shared/layout'; import { WorkplaceSearchNav, WorkplaceSearchHeaderActions } from './components/layout'; -import { GROUPS_PATH, SETUP_GUIDE_PATH, SOURCES_PATH, PERSONAL_SOURCES_PATH } from './routes'; +import { + GROUPS_PATH, + SETUP_GUIDE_PATH, + SOURCES_PATH, + PERSONAL_SOURCES_PATH, + ORG_SETTINGS_PATH, +} from './routes'; import { SetupGuide } from './views/setup_guide'; import { ErrorState } from './views/error_state'; @@ -24,9 +30,11 @@ import { NotFound } from '../shared/not_found'; import { Overview } from './views/overview'; import { GroupsRouter } from './views/groups'; import { SourcesRouter } from './views/content_sources'; +import { SettingsRouter } from './views/settings'; import { GroupSubNav } from './views/groups/components/group_sub_nav'; import { SourceSubNav } from './views/content_sources/components/source_sub_nav'; +import { SettingsSubNav } from './views/settings/components/settings_sub_nav'; export const WorkplaceSearch: React.FC = (props) => { const { config } = useValues(KibanaLogic); @@ -88,6 +96,15 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { + + } />} + restrictWidth + readOnlyMode={readOnlyMode} + > + + + } restrictWidth readOnlyMode={readOnlyMode}> {errorConnecting ? ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.test.tsx new file mode 100644 index 0000000000000..0bc3d3dadd095 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../../__mocks__'; + +import { configuredSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Loading } from '../../../../shared/loading'; +import { LicenseCallout } from '../../../components/shared/license_callout'; + +import { Connectors } from './connectors'; + +describe('Connectors', () => { + const initializeConnectors = jest.fn(); + + beforeEach(() => { + setMockActions({ initializeConnectors }); + setMockValues({ connectors: configuredSources }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="ConnectorRow"]')).toHaveLength(configuredSources.length); + }); + + it('returns loading when loading', () => { + setMockValues({ + connectors: configuredSources, + dataLoading: true, + }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('renders LicenseCallout for restricted items', () => { + const wrapper = shallow(); + + const numUnsupportedAccountOnly = configuredSources.filter( + (s) => s.accountContextOnly && !s.supportedByLicense + ).length; + expect(wrapper.find(LicenseCallout)).toHaveLength(numUnsupportedAccountOnly); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx new file mode 100644 index 0000000000000..4029809a07fe0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; +import { reject } from 'lodash'; + +import { + EuiBadge, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, +} from '@elastic/eui'; + +import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; +import { Loading } from '../../../../shared/loading'; +import { SourceIcon } from '../../../components/shared/source_icon'; +import { LicenseCallout } from '../../../components/shared/license_callout'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; + +import { + CONFIGURE_BUTTON, + CONNECTORS_HEADER_TITLE, + CONNECTORS_HEADER_DESCRIPTION, + CUSTOM_SERVICE_TYPE, + PRIVATE_PLATINUM_LICENSE_CALLOUT, + PRIVATE_SOURCE, + UPDATE_BUTTON, +} from '../../../constants'; +import { getSourcesPath } from '../../../routes'; +import { SourceDataItem } from '../../../types'; + +import { staticSourceData } from '../../content_sources/source_data'; + +import { SettingsLogic } from '../settings_logic'; + +export const Connectors: React.FC = () => { + const { initializeConnectors } = useActions(SettingsLogic); + const { dataLoading, connectors } = useValues(SettingsLogic); + + useEffect(() => { + initializeConnectors(); + }, []); + + if (dataLoading) return ; + + const availableConnectors = reject( + connectors, + ({ serviceType }) => serviceType === CUSTOM_SERVICE_TYPE + ); + + const getRowActions = (configured: boolean, serviceType: string, supportedByLicense: boolean) => { + const { addPath, editPath } = staticSourceData.find( + (s) => s.serviceType === serviceType + ) as SourceDataItem; + const configurePath = getSourcesPath(addPath, true); + + const updateButtons = ( + + + + {UPDATE_BUTTON} + + + + ); + + const configureButton = supportedByLicense ? ( + + {CONFIGURE_BUTTON} + + ) : ( + + {CONFIGURE_BUTTON} + + ); + + return configured ? updateButtons : configureButton; + }; + + const platinumLicenseCallout = ; + + const connectorsList = ( + + {availableConnectors.map( + ({ serviceType, name, configured, accountContextOnly, supportedByLicense }) => ( + + + + + + + + + + + {name} +    + {accountContextOnly && {PRIVATE_SOURCE}} + + + + + + {getRowActions(configured, serviceType, supportedByLicense)} + + + {accountContextOnly && !supportedByLicense && ( + + + + {platinumLicenseCallout} + + + )} + + ) + )} + + ); + + return ( + <> + + {connectorsList} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx new file mode 100644 index 0000000000000..4db0a60b75ee0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiFieldText } from '@elastic/eui'; + +import { ContentSection } from '../../../components/shared/content_section'; + +import { Customize } from './customize'; + +describe('Customize', () => { + const onOrgNameInputChange = jest.fn(); + const updateOrgName = jest.fn(); + + beforeEach(() => { + setMockActions({ onOrgNameInputChange, updateOrgName }); + setMockValues({ orgNameInputValue: '' }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(ContentSection)).toHaveLength(1); + }); + + it('handles input change', () => { + const wrapper = shallow(); + const input = wrapper.find(EuiFieldText); + input.simulate('change', { target: { value: 'foobar' } }); + + expect(onOrgNameInputChange).toHaveBeenCalledWith('foobar'); + }); + + it('handles form submission', () => { + const wrapper = shallow(); + const preventDefault = jest.fn(); + wrapper.find('form').simulate('submit', { preventDefault }); + + expect(updateOrgName).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx new file mode 100644 index 0000000000000..b87e00965f55c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FormEvent } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiButton, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; + +import { + CUSTOMIZE_HEADER_TITLE, + CUSTOMIZE_HEADER_DESCRIPTION, + CUSTOMIZE_NAME_LABEL, + CUSTOMIZE_NAME_BUTTON, +} from '../../../constants'; + +import { ContentSection } from '../../../components/shared/content_section'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; + +import { SettingsLogic } from '../settings_logic'; + +export const Customize: React.FC = () => { + const { onOrgNameInputChange, updateOrgName } = useActions(SettingsLogic); + const { orgNameInputValue } = useValues(SettingsLogic); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + updateOrgName(); + }; + + return ( +
+ + + + + + onOrgNameInputChange(e.target.value)} + /> + + + + + + + {CUSTOMIZE_NAME_BUTTON} + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx new file mode 100644 index 0000000000000..ec831492ee902 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiModal, EuiForm } from '@elastic/eui'; + +import { oauthApplication } from '../../../__mocks__/content_sources.mock'; +import { OAUTH_DESCRIPTION, REDIRECT_INSECURE_ERROR_TEXT } from '../../../constants'; + +import { CredentialItem } from '../../../components/shared/credential_item'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; +import { OauthApplication } from './oauth_application'; + +describe('OauthApplication', () => { + const setOauthApplication = jest.fn(); + const updateOauthApplication = jest.fn(); + + beforeEach(() => { + setMockValues({ hasPlatinumLicense: true, oauthApplication }); + setMockActions({ setOauthApplication, updateOauthApplication }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiForm)).toHaveLength(1); + }); + + it('does not render when no oauthApp', () => { + setMockValues({ hasPlatinumLicense: true, oauthApplication: undefined }); + const wrapper = shallow(); + + expect(wrapper.find(EuiForm)).toHaveLength(0); + }); + + it('handles OAuthAppName change', () => { + const wrapper = shallow(); + const input = wrapper.find('[data-test-subj="OAuthAppName"]'); + input.simulate('change', { target: { value: 'foo' } }); + + expect(setOauthApplication).toHaveBeenCalledWith({ ...oauthApplication, name: 'foo' }); + }); + + it('handles RedirectURIsTextArea change', () => { + const wrapper = shallow(); + const input = wrapper.find('[data-test-subj="RedirectURIsTextArea"]'); + input.simulate('change', { target: { value: 'bar' } }); + + expect(setOauthApplication).toHaveBeenCalledWith({ + ...oauthApplication, + redirectUri: 'bar', + }); + }); + + it('handles ConfidentialToggle change', () => { + const wrapper = shallow(); + const input = wrapper.find('[data-test-subj="ConfidentialToggle"]'); + input.simulate('change', { target: { checked: true } }); + + expect(setOauthApplication).toHaveBeenCalledWith({ + ...oauthApplication, + confidential: true, + }); + }); + + it('handles form submission', () => { + const wrapper = shallow(); + const preventDefault = jest.fn(); + wrapper.find('form').simulate('submit', { preventDefault }); + + expect(updateOauthApplication).toHaveBeenCalled(); + }); + + it('renders ClientSecret on confidential item', () => { + setMockValues({ + hasPlatinumLicense: true, + oauthApplication: { + ...oauthApplication, + confidential: true, + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(CredentialItem)).toHaveLength(2); + }); + + it('renders license modal', () => { + setMockValues({ + hasPlatinumLicense: false, + oauthApplication, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiModal)).toHaveLength(1); + }); + + it('closes license modal', () => { + setMockValues({ + hasPlatinumLicense: false, + oauthApplication, + }); + const wrapper = shallow(); + wrapper.find(EuiModal).prop('onClose')(); + + expect(wrapper.find(EuiModal)).toHaveLength(0); + }); + + it('handles conditional copy', () => { + setMockValues({ + hasPlatinumLicense: true, + oauthApplication: { + ...oauthApplication, + uid: undefined, + redirectUri: 'http://foo', + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(ViewContentHeader).prop('description')).toEqual(OAUTH_DESCRIPTION); + expect(wrapper.find('[data-test-subj="RedirectURIsRow"]').prop('error')).toEqual( + REDIRECT_INSECURE_ERROR_TEXT + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx new file mode 100644 index 0000000000000..b648b2577963a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx @@ -0,0 +1,203 @@ +/* + * 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, { FormEvent, useState } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiForm, + EuiFormRow, + EuiFieldText, + EuiTextArea, + EuiSwitch, + EuiCode, + EuiSpacer, + EuiOverlayMask, + EuiLink, + EuiModal, + EuiModalBody, + EuiTitle, + EuiText, +} from '@elastic/eui'; + +import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes'; +import { + CLIENT_ID_LABEL, + CLIENT_SECRET_LABEL, + CONFIDENTIAL_HELP_TEXT, + CONFIDENTIAL_LABEL, + CREDENTIALS_TITLE, + CREDENTIALS_DESCRIPTION, + NAME_LABEL, + NAV, + OAUTH_DESCRIPTION, + OAUTH_PERSISTED_DESCRIPTION, + REDIRECT_HELP_TEXT, + REDIRECT_NATIVE_HELP_TEXT, + REDIRECT_INSECURE_ERROR_TEXT, + REDIRECT_SECURE_ERROR_TEXT, + REDIRECT_URIS_LABEL, + SAVE_CHANGES_BUTTON, + LICENSE_MODAL_TITLE, + LICENSE_MODAL_DESCRIPTION, + LICENSE_MODAL_LINK, +} from '../../../constants'; + +import { LicensingLogic } from '../../../../shared/licensing'; +import { ContentSection } from '../../../components/shared/content_section'; +import { LicenseBadge } from '../../../components/shared/license_badge'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; +import { CredentialItem } from '../../../components/shared/credential_item'; +import { SettingsLogic } from '../settings_logic'; + +export const OauthApplication: React.FC = () => { + const { setOauthApplication, updateOauthApplication } = useActions(SettingsLogic); + const { oauthApplication } = useValues(SettingsLogic); + const { hasPlatinumLicense } = useValues(LicensingLogic); + + const [isLicenseModalVisible, setIsLicenseModalVisible] = useState(!hasPlatinumLicense); + const closeLicenseModal = () => setIsLicenseModalVisible(false); + + if (!oauthApplication) return null; + + const persisted = !!(oauthApplication.uid && oauthApplication.secret); + const description = persisted ? OAUTH_PERSISTED_DESCRIPTION : OAUTH_DESCRIPTION; + const insecureRedirectUri = /(^|\s)http:/i.test(oauthApplication.redirectUri); + const redirectUris = oauthApplication.redirectUri.split('\n').map((uri) => uri.trim()); + const uniqRedirectUri = Array.from(new Set(redirectUris)); + const redirectUriInvalid = insecureRedirectUri || redirectUris.length !== uniqRedirectUri.length; + + const redirectUriHelpText = ( + + {REDIRECT_HELP_TEXT}{' '} + {oauthApplication.nativeRedirectUri && ( + + {REDIRECT_NATIVE_HELP_TEXT} {oauthApplication.nativeRedirectUri} + + )} + + ); + + const redirectErrorText = insecureRedirectUri + ? REDIRECT_INSECURE_ERROR_TEXT + : REDIRECT_SECURE_ERROR_TEXT; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + updateOauthApplication(); + }; + + const licenseModal = ( + + + + + + + +

{LICENSE_MODAL_TITLE}

+
+ + {LICENSE_MODAL_DESCRIPTION} + + + {LICENSE_MODAL_LINK} + + +
+
+
+ ); + + return ( + <> +
+ + + + + setOauthApplication({ ...oauthApplication, name: e.target.value })} + required + disabled={!hasPlatinumLicense} + /> + + + + + setOauthApplication({ ...oauthApplication, redirectUri: e.target.value }) + } + required + disabled={!hasPlatinumLicense} + /> + + + + + setOauthApplication({ ...oauthApplication, confidential: e.target.checked }) + } + disabled={!hasPlatinumLicense} + /> + + + + {SAVE_CHANGES_BUTTON} + + + {persisted && ( + + + + + + {oauthApplication.confidential && ( + <> + + + + + + )} + + )} + +
+ + {isLicenseModalVisible && licenseModal} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.test.tsx new file mode 100644 index 0000000000000..3a2d4833da689 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.test.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SideNavLink } from '../../../../shared/layout'; + +import { SettingsSubNav } from './settings_sub_nav'; + +describe('SettingsSubNav', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(SideNavLink)).toHaveLength(3); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.tsx new file mode 100644 index 0000000000000..794718044b9fe --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { NAV } from '../../../constants'; + +import { SideNavLink } from '../../../../shared/layout'; + +import { + ORG_SETTINGS_CUSTOMIZE_PATH, + ORG_SETTINGS_CONNECTORS_PATH, + ORG_SETTINGS_OAUTH_APPLICATION_PATH, +} from '../../../routes'; + +export const SettingsSubNav: React.FC = () => ( + <> + {NAV.SETTINGS_CUSTOMIZE} + + {NAV.SETTINGS_SOURCE_PRIORITIZATION} + + {NAV.SETTINGS_OAUTH} + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx new file mode 100644 index 0000000000000..2ecbc2502502a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiConfirmModal } from '@elastic/eui'; + +import { sourceConfigData } from '../../../__mocks__/content_sources.mock'; + +import { Loading } from '../../../../shared/loading'; +import { SaveConfig } from '../../content_sources/components/add_source/save_config'; +import { SourceConfig } from './source_config'; + +describe('SourceConfig', () => { + const deleteSourceConfig = jest.fn(); + const getSourceConfigData = jest.fn(); + const saveSourceConfig = jest.fn(); + + beforeEach(() => { + setMockValues({ sourceConfigData, dataLoading: false }); + setMockActions({ deleteSourceConfig, getSourceConfigData, saveSourceConfig }); + }); + + it('renders', () => { + const wrapper = shallow(); + const saveConfig = wrapper.find(SaveConfig); + + // Trigger modal visibility + saveConfig.prop('onDeleteConfig')!(); + + expect(wrapper.find(EuiConfirmModal)).toHaveLength(1); + }); + + it('returns loading when loading', () => { + setMockValues({ + sourceConfigData, + dataLoading: true, + }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('handles delete click', () => { + const wrapper = shallow(); + const saveConfig = wrapper.find(SaveConfig); + + // Trigger modal visibility + saveConfig.prop('onDeleteConfig')!(); + + wrapper.find(EuiConfirmModal).prop('onConfirm')!({} as any); + + expect(deleteSourceConfig).toHaveBeenCalled(); + }); + + it('saves source config', () => { + const wrapper = shallow(); + const saveConfig = wrapper.find(SaveConfig); + + // Trigger modal visibility + saveConfig.prop('onDeleteConfig')!(); + + saveConfig.prop('advanceStep')!(); + + expect(saveSourceConfig).toHaveBeenCalled(); + }); + + it('cancels and closes modal', () => { + const wrapper = shallow(); + const saveConfig = wrapper.find(SaveConfig); + + // Trigger modal visibility + saveConfig.prop('onDeleteConfig')!(); + + wrapper.find(EuiConfirmModal).prop('onCancel')!({} as any); + + expect(wrapper.find(EuiConfirmModal)).toHaveLength(0); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx new file mode 100644 index 0000000000000..52e87311284ff --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; + +import { useActions, useValues } from 'kea'; +import { i18n } from '@kbn/i18n'; + +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; + +import { Loading } from '../../../../shared/loading'; +import { SourceDataItem } from '../../../types'; +import { staticSourceData } from '../../content_sources/source_data'; +import { SourceLogic } from '../../content_sources/source_logic'; +import { AddSourceLogic } from '../../content_sources/components/add_source/add_source_logic'; + +import { AddSourceHeader } from '../../content_sources/components/add_source/add_source_header'; +import { SaveConfig } from '../../content_sources/components/add_source/save_config'; + +import { SettingsLogic } from '../settings_logic'; + +interface SourceConfigProps { + sourceIndex: number; +} + +export const SourceConfig: React.FC = ({ sourceIndex }) => { + const [confirmModalVisible, setConfirmModalVisibility] = useState(false); + const { configuration, serviceType } = staticSourceData[sourceIndex] as SourceDataItem; + const { deleteSourceConfig } = useActions(SettingsLogic); + const { getSourceConfigData } = useActions(SourceLogic); + const { saveSourceConfig } = useActions(AddSourceLogic); + const { + sourceConfigData: { name, categories }, + dataLoading: sourceDataLoading, + } = useValues(SourceLogic); + + useEffect(() => { + getSourceConfigData(serviceType); + }, []); + + if (sourceDataLoading) return ; + const hideConfirmModal = () => setConfirmModalVisibility(false); + const showConfirmModal = () => setConfirmModalVisibility(true); + const saveUpdatedConfig = () => saveSourceConfig(true); + + const header = ; + + return ( + <> + + {confirmModalVisible && ( + + deleteSourceConfig(serviceType, name)} + onCancel={hideConfirmModal} + buttonColor="danger" + > + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.confirmRemoveConfig.message', + { + defaultMessage: + 'Are you sure you want to remove the OAuth configuration for {name}?', + values: { name }, + } + )} + + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/index.ts new file mode 100644 index 0000000000000..cfea0684c1059 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { SettingsRouter } from './settings_router'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts new file mode 100644 index 0000000000000..aaeae08d552d4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts @@ -0,0 +1,234 @@ +/* + * 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 { LogicMounter } from '../../../__mocks__/kea.mock'; + +import { + mockFlashMessageHelpers, + mockHttpValues, + expectedAsyncError, + mockKibanaValues, +} from '../../../__mocks__'; + +import { configuredSources, oauthApplication } from '../../__mocks__/content_sources.mock'; + +import { ORG_UPDATED_MESSAGE, OAUTH_APP_UPDATED_MESSAGE } from '../../constants'; +import { SettingsLogic } from './settings_logic'; + +describe('SettingsLogic', () => { + const { http } = mockHttpValues; + const { navigateToUrl } = mockKibanaValues; + const { + clearFlashMessages, + flashAPIErrors, + setSuccessMessage, + setQueuedSuccessMessage, + } = mockFlashMessageHelpers; + const { mount } = new LogicMounter(SettingsLogic); + const ORG_NAME = 'myOrg'; + const defaultValues = { + dataLoading: true, + connectors: [], + orgNameInputValue: '', + oauthApplication: null, + }; + const serverProps = { organizationName: ORG_NAME, oauthApplication }; + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(SettingsLogic.values).toEqual(defaultValues); + }); + + describe('actions', () => { + it('onInitializeConnectors', () => { + SettingsLogic.actions.onInitializeConnectors(configuredSources); + }); + + it('onOrgNameInputChange', () => { + const NAME = 'foo'; + SettingsLogic.actions.onOrgNameInputChange(NAME); + + expect(SettingsLogic.values.orgNameInputValue).toEqual(NAME); + }); + + it('setUpdatedName', () => { + const NAME = 'bar'; + SettingsLogic.actions.setUpdatedName({ organizationName: NAME }); + + expect(SettingsLogic.values.orgNameInputValue).toEqual(NAME); + }); + + it('setServerProps', () => { + SettingsLogic.actions.setServerProps(serverProps); + + expect(SettingsLogic.values.orgNameInputValue).toEqual(ORG_NAME); + expect(SettingsLogic.values.oauthApplication).toEqual(oauthApplication); + }); + + it('setOauthApplication', () => { + SettingsLogic.actions.setOauthApplication(oauthApplication); + + expect(SettingsLogic.values.oauthApplication).toEqual(oauthApplication); + }); + + it('setUpdatedOauthApplication', () => { + SettingsLogic.actions.setUpdatedOauthApplication({ oauthApplication }); + + expect(SettingsLogic.values.oauthApplication).toEqual(oauthApplication); + }); + }); + + describe('listeners', () => { + describe('initializeSettings', () => { + it('calls API and sets values', async () => { + const setServerPropsSpy = jest.spyOn(SettingsLogic.actions, 'setServerProps'); + const promise = Promise.resolve(configuredSources); + http.get.mockReturnValue(promise); + SettingsLogic.actions.initializeSettings(); + + expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/settings'); + await promise; + expect(setServerPropsSpy).toHaveBeenCalledWith(configuredSources); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + http.get.mockReturnValue(promise); + SettingsLogic.actions.initializeSettings(); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + describe('initializeConnectors', () => { + it('calls API and sets values', async () => { + const onInitializeConnectorsSpy = jest.spyOn( + SettingsLogic.actions, + 'onInitializeConnectors' + ); + const promise = Promise.resolve(serverProps); + http.get.mockReturnValue(promise); + SettingsLogic.actions.initializeConnectors(); + + expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/settings/connectors'); + await promise; + expect(onInitializeConnectorsSpy).toHaveBeenCalledWith(serverProps); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + http.get.mockReturnValue(promise); + SettingsLogic.actions.initializeConnectors(); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + describe('updateOrgName', () => { + it('calls API and sets values', async () => { + const NAME = 'updated name'; + SettingsLogic.actions.onOrgNameInputChange(NAME); + const setUpdatedNameSpy = jest.spyOn(SettingsLogic.actions, 'setUpdatedName'); + const promise = Promise.resolve({ organizationName: NAME }); + http.put.mockReturnValue(promise); + + SettingsLogic.actions.updateOrgName(); + + expect(http.put).toHaveBeenCalledWith('/api/workplace_search/org/settings/customize', { + body: JSON.stringify({ name: NAME }), + }); + await promise; + expect(setSuccessMessage).toHaveBeenCalledWith(ORG_UPDATED_MESSAGE); + expect(setUpdatedNameSpy).toHaveBeenCalledWith({ organizationName: NAME }); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + http.put.mockReturnValue(promise); + SettingsLogic.actions.updateOrgName(); + + await expectedAsyncError(promise); + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + describe('updateOauthApplication', () => { + it('calls API and sets values', async () => { + const { name, redirectUri, confidential } = oauthApplication; + const setUpdatedOauthApplicationSpy = jest.spyOn( + SettingsLogic.actions, + 'setUpdatedOauthApplication' + ); + const promise = Promise.resolve({ oauthApplication }); + http.put.mockReturnValue(promise); + SettingsLogic.actions.setOauthApplication(oauthApplication); + SettingsLogic.actions.updateOauthApplication(); + + expect(clearFlashMessages).toHaveBeenCalled(); + + expect(http.put).toHaveBeenCalledWith( + '/api/workplace_search/org/settings/oauth_application', + { + body: JSON.stringify({ + oauth_application: { name, confidential, redirect_uri: redirectUri }, + }), + } + ); + await promise; + expect(setUpdatedOauthApplicationSpy).toHaveBeenCalledWith({ oauthApplication }); + expect(setSuccessMessage).toHaveBeenCalledWith(OAUTH_APP_UPDATED_MESSAGE); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + http.put.mockReturnValue(promise); + SettingsLogic.actions.updateOauthApplication(); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + describe('deleteSourceConfig', () => { + const SERVICE_TYPE = 'github'; + const NAME = 'baz'; + + it('calls API and sets values', async () => { + const promise = Promise.resolve({}); + http.delete.mockReturnValue(promise); + SettingsLogic.actions.deleteSourceConfig(SERVICE_TYPE, NAME); + + await promise; + expect(navigateToUrl).toHaveBeenCalledWith('/settings/connectors'); + expect(setQueuedSuccessMessage).toHaveBeenCalled(); + }); + + it('handles error', async () => { + const promise = Promise.reject('this is an error'); + http.delete.mockReturnValue(promise); + SettingsLogic.actions.deleteSourceConfig(SERVICE_TYPE, NAME); + await expectedAsyncError(promise); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + it('resetSettingsState', () => { + // needed to set dataLoading to false + SettingsLogic.actions.onInitializeConnectors(configuredSources); + SettingsLogic.actions.resetSettingsState(); + + expect(clearFlashMessages).toHaveBeenCalled(); + expect(SettingsLogic.values.dataLoading).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts new file mode 100644 index 0000000000000..b5370ec09f670 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts @@ -0,0 +1,197 @@ +/* + * 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 { kea, MakeLogicType } from 'kea'; +import { i18n } from '@kbn/i18n'; + +import { + clearFlashMessages, + setQueuedSuccessMessage, + setSuccessMessage, + flashAPIErrors, +} from '../../../shared/flash_messages'; +import { KibanaLogic } from '../../../shared/kibana'; +import { HttpLogic } from '../../../shared/http'; + +import { Connector } from '../../types'; +import { ORG_UPDATED_MESSAGE, OAUTH_APP_UPDATED_MESSAGE } from '../../constants'; + +import { ORG_SETTINGS_CONNECTORS_PATH } from '../../routes'; + +interface IOauthApplication { + name: string; + uid: string; + secret: string; + redirectUri: string; + confidential: boolean; + nativeRedirectUri: string; +} + +export interface SettingsServerProps { + organizationName: string; + oauthApplication: IOauthApplication; +} + +interface SettingsActions { + onInitializeConnectors(connectors: Connector[]): Connector[]; + onOrgNameInputChange(orgNameInputValue: string): string; + setUpdatedName({ organizationName }: { organizationName: string }): string; + setServerProps(props: SettingsServerProps): SettingsServerProps; + setOauthApplication(oauthApplication: IOauthApplication): IOauthApplication; + setUpdatedOauthApplication({ + oauthApplication, + }: { + oauthApplication: IOauthApplication; + }): IOauthApplication; + resetSettingsState(): void; + initializeSettings(): void; + initializeConnectors(): void; + updateOauthApplication(): void; + updateOrgName(): void; + deleteSourceConfig( + serviceType: string, + name: string + ): { + serviceType: string; + name: string; + }; +} + +interface SettingsValues { + dataLoading: boolean; + connectors: Connector[]; + orgNameInputValue: string; + oauthApplication: IOauthApplication | null; +} + +export const SettingsLogic = kea>({ + actions: { + onInitializeConnectors: (connectors: Connector[]) => connectors, + onOrgNameInputChange: (orgNameInputValue: string) => orgNameInputValue, + setUpdatedName: ({ organizationName }) => organizationName, + setServerProps: (props: SettingsServerProps) => props, + setOauthApplication: (oauthApplication: IOauthApplication) => oauthApplication, + setUpdatedOauthApplication: ({ oauthApplication }: { oauthApplication: IOauthApplication }) => + oauthApplication, + resetSettingsState: () => true, + initializeSettings: () => true, + initializeConnectors: () => true, + updateOrgName: () => true, + updateOauthApplication: () => true, + deleteSourceConfig: (serviceType: string, name: string) => ({ + serviceType, + name, + }), + }, + reducers: { + connectors: [ + [], + { + onInitializeConnectors: (_, connectors) => connectors, + }, + ], + orgNameInputValue: [ + '', + { + setServerProps: (_, { organizationName }) => organizationName, + onOrgNameInputChange: (_, orgNameInputValue) => orgNameInputValue, + setUpdatedName: (_, organizationName) => organizationName, + }, + ], + oauthApplication: [ + null, + { + setServerProps: (_, { oauthApplication }) => oauthApplication, + setOauthApplication: (_, oauthApplication) => oauthApplication, + setUpdatedOauthApplication: (_, oauthApplication) => oauthApplication, + }, + ], + dataLoading: [ + true, + { + onInitializeConnectors: () => false, + resetSettingsState: () => true, + }, + ], + }, + listeners: ({ actions, values }) => ({ + initializeSettings: async () => { + const { http } = HttpLogic.values; + const route = '/api/workplace_search/org/settings'; + + try { + const response = await http.get(route); + actions.setServerProps(response); + } catch (e) { + flashAPIErrors(e); + } + }, + initializeConnectors: async () => { + const { http } = HttpLogic.values; + const route = '/api/workplace_search/org/settings/connectors'; + + try { + const response = await http.get(route); + actions.onInitializeConnectors(response); + } catch (e) { + flashAPIErrors(e); + } + }, + updateOrgName: async () => { + const { http } = HttpLogic.values; + const route = '/api/workplace_search/org/settings/customize'; + const { orgNameInputValue: name } = values; + const body = JSON.stringify({ name }); + + try { + const response = await http.put(route, { body }); + actions.setUpdatedName(response); + setSuccessMessage(ORG_UPDATED_MESSAGE); + } catch (e) { + flashAPIErrors(e); + } + }, + updateOauthApplication: async () => { + const { http } = HttpLogic.values; + const route = '/api/workplace_search/org/settings/oauth_application'; + const oauthApplication = values.oauthApplication || ({} as IOauthApplication); + const { name, redirectUri, confidential } = oauthApplication; + const body = JSON.stringify({ + oauth_application: { name, confidential, redirect_uri: redirectUri }, + }); + + clearFlashMessages(); + + try { + const response = await http.put(route, { body }); + actions.setUpdatedOauthApplication(response); + setSuccessMessage(OAUTH_APP_UPDATED_MESSAGE); + } catch (e) { + flashAPIErrors(e); + } + }, + deleteSourceConfig: async ({ serviceType, name }) => { + const { http } = HttpLogic.values; + const route = `/api/workplace_search/org/settings/connectors/${serviceType}`; + + try { + await http.delete(route); + KibanaLogic.values.navigateToUrl(ORG_SETTINGS_CONNECTORS_PATH); + setQueuedSuccessMessage( + i18n.translate('xpack.enterpriseSearch.workplaceSearch.settings.configRemoved.message', { + defaultMessage: 'Successfully removed configuration for {name}.', + values: { name }, + }) + ); + } catch (e) { + flashAPIErrors(e); + } + }, + resetSettingsState: () => { + clearFlashMessages(); + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx new file mode 100644 index 0000000000000..315f6c4561cc5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockActions } from '../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Route, Redirect, Switch } from 'react-router-dom'; + +import { staticSourceData } from '../content_sources/source_data'; + +import { FlashMessages } from '../../../shared/flash_messages'; + +import { Connectors } from './components/connectors'; +import { Customize } from './components/customize'; +import { OauthApplication } from './components/oauth_application'; +import { SourceConfig } from './components/source_config'; + +import { SettingsRouter } from './settings_router'; + +describe('SettingsRouter', () => { + const initializeSettings = jest.fn(); + const NUM_SOURCES = staticSourceData.length; + // Should be 3 routes other than the sources listed Connectors, Customize, & OauthApplication + const NUM_ROUTES = NUM_SOURCES + 3; + + beforeEach(() => { + setMockActions({ initializeSettings }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(FlashMessages)).toHaveLength(1); + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(NUM_ROUTES); + expect(wrapper.find(Redirect)).toHaveLength(1); + expect(wrapper.find(Connectors)).toHaveLength(1); + expect(wrapper.find(Customize)).toHaveLength(1); + expect(wrapper.find(OauthApplication)).toHaveLength(1); + expect(wrapper.find(SourceConfig)).toHaveLength(NUM_SOURCES); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx new file mode 100644 index 0000000000000..c5df8b3c7aa28 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx @@ -0,0 +1,59 @@ +/* + * 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, { useEffect } from 'react'; + +import { useActions } from 'kea'; +import { Redirect, Route, Switch } from 'react-router-dom'; + +import { + ORG_SETTINGS_PATH, + ORG_SETTINGS_CUSTOMIZE_PATH, + ORG_SETTINGS_CONNECTORS_PATH, + ORG_SETTINGS_OAUTH_APPLICATION_PATH, +} from '../../routes'; + +import { FlashMessages } from '../../../shared/flash_messages'; + +import { Connectors } from './components/connectors'; +import { Customize } from './components/customize'; +import { OauthApplication } from './components/oauth_application'; +import { SourceConfig } from './components/source_config'; + +import { staticSourceData } from '../content_sources/source_data'; + +import { SettingsLogic } from './settings_logic'; + +export const SettingsRouter: React.FC = () => { + const { initializeSettings } = useActions(SettingsLogic); + + useEffect(() => { + initializeSettings(); + }, []); + + return ( + <> + + + + + + + + + + + + + {staticSourceData.map(({ editPath }, i) => ( + + + + ))} + + + ); +}; From 6e4c70848dc189c8a10f20ac2a348dd440ce9118 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 18 Jan 2021 21:47:53 +0100 Subject: [PATCH 14/23] [Core] Remove public context (#88448) * remove client side context * update docs * fix tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/kibana-plugin-core-public.app.md | 2 +- .../kibana-plugin-core-public.app.mount.md | 9 +- ...ana-plugin-core-public.applicationsetup.md | 1 - ...c.applicationsetup.registermountcontext.md | 29 ---- ...ana-plugin-core-public.applicationstart.md | 1 - ...c.applicationstart.registermountcontext.md | 29 ---- ...plugin-core-public.appmountcontext.core.md | 26 ---- ...bana-plugin-core-public.appmountcontext.md | 24 --- ...a-plugin-core-public.appmountdeprecated.md | 22 --- ...lic.contextsetup.createcontextcontainer.md | 17 --- .../kibana-plugin-core-public.contextsetup.md | 138 ------------------ ...na-plugin-core-public.coresetup.context.md | 17 --- .../kibana-plugin-core-public.coresetup.md | 1 - ...a-plugin-core-public.handlercontexttype.md | 13 -- ...bana-plugin-core-public.handlerfunction.md | 13 -- ...na-plugin-core-public.handlerparameters.md | 13 -- ...-public.icontextcontainer.createhandler.md | 27 ---- ...na-plugin-core-public.icontextcontainer.md | 80 ---------- ...ublic.icontextcontainer.registercontext.md | 34 ----- ...ana-plugin-core-public.icontextprovider.md | 18 --- .../core/public/kibana-plugin-core-public.md | 8 - .../kibana-plugin-plugins-data-server.md | 2 +- ...ugin-plugins-data-server.sessionservice.md | 2 +- examples/embeddable_explorer/public/app.tsx | 3 +- .../application/application_service.mock.ts | 4 - .../application/application_service.test.ts | 14 -- .../application/application_service.tsx | 27 +--- src/core/public/application/index.ts | 2 - .../application_service.test.tsx | 2 - src/core/public/application/types.ts | 137 +---------------- .../header/__snapshots__/header.test.tsx.snap | 1 - .../public/context/context_service.mock.ts | 42 ------ .../context/context_service.test.mocks.ts | 25 ---- .../public/context/context_service.test.ts | 37 ----- src/core/public/context/context_service.ts | 107 -------------- src/core/public/context/index.ts | 27 ---- src/core/public/core_system.test.mocks.ts | 7 - src/core/public/core_system.test.ts | 24 --- src/core/public/core_system.ts | 25 +--- src/core/public/index.ts | 21 --- src/core/public/mocks.ts | 2 - src/core/public/plugins/plugin_context.ts | 5 - .../public/plugins/plugins_service.test.ts | 2 - src/core/public/public.api.md | 57 +------- src/plugins/embeddable/public/public.api.md | 1 - .../saved_objects_table.test.tsx.snap | 1 - .../kbn_tp_run_pipeline/public/app/app.tsx | 4 +- .../kbn_tp_run_pipeline/public/plugin.ts | 4 +- .../core_plugin_a/public/application.tsx | 12 +- .../plugins/core_plugin_a/public/plugin.tsx | 5 +- .../core_plugin_appleave/public/plugin.tsx | 4 +- .../core_plugin_b/public/application.tsx | 12 +- .../plugins/core_plugin_b/public/plugin.tsx | 5 +- .../public/application.tsx | 11 +- .../core_plugin_chromeless/public/plugin.tsx | 4 +- .../core_plugin_helpmenu/public/plugin.tsx | 2 +- .../rendering_plugin/public/plugin.tsx | 4 +- x-pack/plugins/graph/public/application.ts | 7 +- x-pack/plugins/graph/public/plugin.ts | 2 +- .../monitoring/public/angular/app_modules.ts | 4 +- .../public/pages/overview/empty_section.ts | 4 +- .../public/services/get_news_feed.test.ts | 6 +- .../public/services/get_news_feed.ts | 4 +- 63 files changed, 60 insertions(+), 1133 deletions(-) delete mode 100644 docs/development/core/public/kibana-plugin-core-public.applicationsetup.registermountcontext.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.applicationstart.registermountcontext.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.appmountcontext.core.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.appmountcontext.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.appmountdeprecated.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.contextsetup.createcontextcontainer.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.contextsetup.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.coresetup.context.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.handlercontexttype.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.handlerfunction.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.handlerparameters.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.icontextcontainer.createhandler.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.icontextcontainer.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.icontextcontainer.registercontext.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.icontextprovider.md delete mode 100644 src/core/public/context/context_service.mock.ts delete mode 100644 src/core/public/context/context_service.test.mocks.ts delete mode 100644 src/core/public/context/context_service.test.ts delete mode 100644 src/core/public/context/context_service.ts delete mode 100644 src/core/public/context/index.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.app.md b/docs/development/core/public/kibana-plugin-core-public.app.md index b24ced68b7d38..9a508f293d8e8 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.md @@ -25,7 +25,7 @@ export interface App | [icon](./kibana-plugin-core-public.app.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | | [id](./kibana-plugin-core-public.app.id.md) | string | The unique identifier of the application | | [meta](./kibana-plugin-core-public.app.meta.md) | AppMeta | Meta data for an application that represent additional information for the app. See [AppMeta](./kibana-plugin-core-public.appmeta.md) | -| [mount](./kibana-plugin-core-public.app.mount.md) | AppMount<HistoryLocationState> | AppMountDeprecated<HistoryLocationState> | A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-core-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-core-public.appmountdeprecated.md). | +| [mount](./kibana-plugin-core-public.app.mount.md) | AppMount<HistoryLocationState> | A mount function called when the user navigates to this app's route. | | [navLinkStatus](./kibana-plugin-core-public.app.navlinkstatus.md) | AppNavLinkStatus | The initial status of the application's navLink. Defaulting to visible if status is accessible and hidden if status is inaccessible See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) | | [order](./kibana-plugin-core-public.app.order.md) | number | An ordinal used to sort nav links relative to one another for display. | | [status](./kibana-plugin-core-public.app.status.md) | AppStatus | The initial status of the application. Defaulting to accessible | diff --git a/docs/development/core/public/kibana-plugin-core-public.app.mount.md b/docs/development/core/public/kibana-plugin-core-public.app.mount.md index 8a9dfd9e2e972..460ded2a4bff1 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.mount.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.mount.md @@ -4,15 +4,10 @@ ## App.mount property -A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-core-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-core-public.appmountdeprecated.md). +A mount function called when the user navigates to this app's route. Signature: ```typescript -mount: AppMount | AppMountDeprecated; +mount: AppMount; ``` - -## Remarks - -When function has two arguments, it will be called with a [context](./kibana-plugin-core-public.appmountcontext.md) as the first argument. This behavior is \*\*deprecated\*\*, and consumers should instead use [CoreSetup.getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md). - diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationsetup.md b/docs/development/core/public/kibana-plugin-core-public.applicationsetup.md index fc99e2208220f..2e8702a56f309 100644 --- a/docs/development/core/public/kibana-plugin-core-public.applicationsetup.md +++ b/docs/development/core/public/kibana-plugin-core-public.applicationsetup.md @@ -17,5 +17,4 @@ export interface ApplicationSetup | --- | --- | | [register(app)](./kibana-plugin-core-public.applicationsetup.register.md) | Register an mountable application to the system. | | [registerAppUpdater(appUpdater$)](./kibana-plugin-core-public.applicationsetup.registerappupdater.md) | Register an application updater that can be used to change the [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) fields of all applications at runtime.This is meant to be used by plugins that needs to updates the whole list of applications. To only updates a specific application, use the updater$ property of the registered application instead. | -| [registerMountContext(contextName, provider)](./kibana-plugin-core-public.applicationsetup.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md). | diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationsetup.registermountcontext.md b/docs/development/core/public/kibana-plugin-core-public.applicationsetup.registermountcontext.md deleted file mode 100644 index 1735d5df943ae..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.applicationsetup.registermountcontext.md +++ /dev/null @@ -1,29 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) > [registerMountContext](./kibana-plugin-core-public.applicationsetup.registermountcontext.md) - -## ApplicationSetup.registerMountContext() method - -> Warning: This API is now obsolete. -> -> - -Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md). - -Signature: - -```typescript -registerMountContext(contextName: T, provider: IContextProvider): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| contextName | T | The key of [AppMountContext](./kibana-plugin-core-public.appmountcontext.md) this provider's return value should be attached to. | -| provider | IContextProvider<AppMountDeprecated, T> | A [IContextProvider](./kibana-plugin-core-public.icontextprovider.md) function | - -Returns: - -`void` - diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.md index ae62a7767a0e9..993234d4c6e09 100644 --- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.md @@ -26,5 +26,4 @@ export interface ApplicationStart | [getUrlForApp(appId, options)](./kibana-plugin-core-public.applicationstart.geturlforapp.md) | Returns the absolute path (or URL) to a given app, including the global base path.By default, it returns the absolute path of the application (e.g /basePath/app/my-app). Use the absolute option to generate an absolute url instead (e.g http://host:port/basePath/app/my-app)Note that when generating absolute urls, the origin (protocol, host and port) are determined from the browser's current location. | | [navigateToApp(appId, options)](./kibana-plugin-core-public.applicationstart.navigatetoapp.md) | Navigate to a given app | | [navigateToUrl(url)](./kibana-plugin-core-public.applicationstart.navigatetourl.md) | Navigate to given URL in a SPA friendly way when possible (when the URL will redirect to a valid application within the current basePath).The method resolves pathnames the same way browsers do when resolving a <a href> value. The provided url can be: - an absolute URL - an absolute path - a path relative to the current URL (window.location.href)If all these criteria are true for the given URL: - (only for absolute URLs) The origin of the URL matches the origin of the browser's current location - The resolved pathname of the provided URL/path starts with the current basePath (eg. /mybasepath/s/my-space) - The pathname segment after the basePath matches any known application route (eg. /app// or any application's appRoute configuration)Then a SPA navigation will be performed using navigateToApp using the corresponding application and path. Otherwise, fallback to a full page reload to navigate to the url using window.location.assign | -| [registerMountContext(contextName, provider)](./kibana-plugin-core-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md). | diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.registermountcontext.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.registermountcontext.md deleted file mode 100644 index 11f661c4af2b3..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.registermountcontext.md +++ /dev/null @@ -1,29 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) > [registerMountContext](./kibana-plugin-core-public.applicationstart.registermountcontext.md) - -## ApplicationStart.registerMountContext() method - -> Warning: This API is now obsolete. -> -> - -Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md). - -Signature: - -```typescript -registerMountContext(contextName: T, provider: IContextProvider): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| contextName | T | The key of [AppMountContext](./kibana-plugin-core-public.appmountcontext.md) this provider's return value should be attached to. | -| provider | IContextProvider<AppMountDeprecated, T> | A [IContextProvider](./kibana-plugin-core-public.icontextprovider.md) function | - -Returns: - -`void` - diff --git a/docs/development/core/public/kibana-plugin-core-public.appmountcontext.core.md b/docs/development/core/public/kibana-plugin-core-public.appmountcontext.core.md deleted file mode 100644 index 2f10fbb3075d5..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appmountcontext.core.md +++ /dev/null @@ -1,26 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppMountContext](./kibana-plugin-core-public.appmountcontext.md) > [core](./kibana-plugin-core-public.appmountcontext.core.md) - -## AppMountContext.core property - -Core service APIs available to mounted applications. - -Signature: - -```typescript -core: { - application: Pick; - chrome: ChromeStart; - docLinks: DocLinksStart; - http: HttpStart; - i18n: I18nStart; - notifications: NotificationsStart; - overlays: OverlayStart; - savedObjects: SavedObjectsStart; - uiSettings: IUiSettingsClient; - injectedMetadata: { - getInjectedVar: (name: string, defaultValue?: any) => unknown; - }; - }; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appmountcontext.md b/docs/development/core/public/kibana-plugin-core-public.appmountcontext.md deleted file mode 100644 index 52a36b0b56f02..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appmountcontext.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppMountContext](./kibana-plugin-core-public.appmountcontext.md) - -## AppMountContext interface - -> Warning: This API is now obsolete. -> -> - -The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md). - -Signature: - -```typescript -export interface AppMountContext -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [core](./kibana-plugin-core-public.appmountcontext.core.md) | {
application: Pick<ApplicationStart, 'capabilities' | 'navigateToApp'>;
chrome: ChromeStart;
docLinks: DocLinksStart;
http: HttpStart;
i18n: I18nStart;
notifications: NotificationsStart;
overlays: OverlayStart;
savedObjects: SavedObjectsStart;
uiSettings: IUiSettingsClient;
injectedMetadata: {
getInjectedVar: (name: string, defaultValue?: any) => unknown;
};
} | Core service APIs available to mounted applications. | - diff --git a/docs/development/core/public/kibana-plugin-core-public.appmountdeprecated.md b/docs/development/core/public/kibana-plugin-core-public.appmountdeprecated.md deleted file mode 100644 index 66b8a69d84a38..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appmountdeprecated.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppMountDeprecated](./kibana-plugin-core-public.appmountdeprecated.md) - -## AppMountDeprecated type - -> Warning: This API is now obsolete. -> -> - -A mount function called when the user navigates to this app's route. - -Signature: - -```typescript -export declare type AppMountDeprecated = (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; -``` - -## Remarks - -When function has two arguments, it will be called with a [context](./kibana-plugin-core-public.appmountcontext.md) as the first argument. This behavior is \*\*deprecated\*\*, and consumers should instead use [CoreSetup.getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md). - diff --git a/docs/development/core/public/kibana-plugin-core-public.contextsetup.createcontextcontainer.md b/docs/development/core/public/kibana-plugin-core-public.contextsetup.createcontextcontainer.md deleted file mode 100644 index da7835cc0f39a..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.contextsetup.createcontextcontainer.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ContextSetup](./kibana-plugin-core-public.contextsetup.md) > [createContextContainer](./kibana-plugin-core-public.contextsetup.createcontextcontainer.md) - -## ContextSetup.createContextContainer() method - -Creates a new [IContextContainer](./kibana-plugin-core-public.icontextcontainer.md) for a service owner. - -Signature: - -```typescript -createContextContainer>(): IContextContainer; -``` -Returns: - -`IContextContainer` - diff --git a/docs/development/core/public/kibana-plugin-core-public.contextsetup.md b/docs/development/core/public/kibana-plugin-core-public.contextsetup.md deleted file mode 100644 index 681e74c923c5e..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.contextsetup.md +++ /dev/null @@ -1,138 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ContextSetup](./kibana-plugin-core-public.contextsetup.md) - -## ContextSetup interface - -An object that handles registration of context providers and configuring handlers with context. - -Signature: - -```typescript -export interface ContextSetup -``` - -## Remarks - -A [IContextContainer](./kibana-plugin-core-public.icontextcontainer.md) can be used by any Core service or plugin (known as the "service owner") which wishes to expose APIs in a handler function. The container object will manage registering context providers and configuring a handler with all of the contexts that should be exposed to the handler's plugin. This is dependent on the dependencies that the handler's plugin declares. - -Contexts providers are executed in the order they were registered. Each provider gets access to context values provided by any plugins that it depends on. - -In order to configure a handler with context, you must call the [IContextContainer.createHandler()](./kibana-plugin-core-public.icontextcontainer.createhandler.md) function and use the returned handler which will automatically build a context object when called. - -When registering context or creating handlers, the \_calling plugin's opaque id\_ must be provided. This id is passed in via the plugin's initializer and can be accessed from the [PluginInitializerContext.opaqueId](./kibana-plugin-core-public.plugininitializercontext.opaqueid.md) Note this should NOT be the context service owner's id, but the plugin that is actually registering the context or handler. - -```ts -// Correct -class MyPlugin { - private readonly handlers = new Map(); - - setup(core) { - this.contextContainer = core.context.createContextContainer(); - return { - registerContext(pluginOpaqueId, contextName, provider) { - this.contextContainer.registerContext(pluginOpaqueId, contextName, provider); - }, - registerRoute(pluginOpaqueId, path, handler) { - this.handlers.set( - path, - this.contextContainer.createHandler(pluginOpaqueId, handler) - ); - } - } - } -} - -// Incorrect -class MyPlugin { - private readonly handlers = new Map(); - - constructor(private readonly initContext: PluginInitializerContext) {} - - setup(core) { - this.contextContainer = core.context.createContextContainer(); - return { - registerContext(contextName, provider) { - // BUG! - // This would leak this context to all handlers rather that only plugins that depend on the calling plugin. - this.contextContainer.registerContext(this.initContext.opaqueId, contextName, provider); - }, - registerRoute(path, handler) { - this.handlers.set( - path, - // BUG! - // This handler will not receive any contexts provided by other dependencies of the calling plugin. - this.contextContainer.createHandler(this.initContext.opaqueId, handler) - ); - } - } - } -} - -``` - -## Example - -Say we're creating a plugin for rendering visualizations that allows new rendering methods to be registered. If we want to offer context to these rendering methods, we can leverage the ContextService to manage these contexts. - -```ts -export interface VizRenderContext { - core: { - i18n: I18nStart; - uiSettings: IUiSettingsClient; - } - [contextName: string]: unknown; -} - -export type VizRenderer = (context: VizRenderContext, domElement: HTMLElement) => () => void; -// When a renderer is bound via `contextContainer.createHandler` this is the type that will be returned. -type BoundVizRenderer = (domElement: HTMLElement) => () => void; - -class VizRenderingPlugin { - private readonly contextContainer?: IContextContainer; - private readonly vizRenderers = new Map(); - - constructor(private readonly initContext: PluginInitializerContext) {} - - setup(core) { - this.contextContainer = core.context.createContextContainer(); - - return { - registerContext: this.contextContainer.registerContext, - registerVizRenderer: (plugin: PluginOpaqueId, renderMethod: string, renderer: VizTypeRenderer) => - this.vizRenderers.set(renderMethod, this.contextContainer.createHandler(plugin, renderer)), - }; - } - - start(core) { - // Register the core context available to all renderers. Use the VizRendererContext's opaqueId as the first arg. - this.contextContainer.registerContext(this.initContext.opaqueId, 'core', () => ({ - i18n: core.i18n, - uiSettings: core.uiSettings - })); - - return { - registerContext: this.contextContainer.registerContext, - - renderVizualization: (renderMethod: string, domElement: HTMLElement) => { - if (!this.vizRenderer.has(renderMethod)) { - throw new Error(`Render method '${renderMethod}' has not been registered`); - } - - // The handler can now be called directly with only an `HTMLElement` and will automatically - // have a new `context` object created and populated by the context container. - const handler = this.vizRenderers.get(renderMethod) - return handler(domElement); - } - }; - } -} - -``` - -## Methods - -| Method | Description | -| --- | --- | -| [createContextContainer()](./kibana-plugin-core-public.contextsetup.createcontextcontainer.md) | Creates a new [IContextContainer](./kibana-plugin-core-public.icontextcontainer.md) for a service owner. | - diff --git a/docs/development/core/public/kibana-plugin-core-public.coresetup.context.md b/docs/development/core/public/kibana-plugin-core-public.coresetup.context.md deleted file mode 100644 index c571f00cecccf..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.coresetup.context.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [CoreSetup](./kibana-plugin-core-public.coresetup.md) > [context](./kibana-plugin-core-public.coresetup.context.md) - -## CoreSetup.context property - -> Warning: This API is now obsolete. -> -> - -[ContextSetup](./kibana-plugin-core-public.contextsetup.md) - -Signature: - -```typescript -context: ContextSetup; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.coresetup.md b/docs/development/core/public/kibana-plugin-core-public.coresetup.md index b9f97b83af88f..5d2288120da05 100644 --- a/docs/development/core/public/kibana-plugin-core-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-core-public.coresetup.md @@ -17,7 +17,6 @@ export interface CoreSetupApplicationSetup | [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) | -| [context](./kibana-plugin-core-public.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-core-public.contextsetup.md) | | [fatalErrors](./kibana-plugin-core-public.coresetup.fatalerrors.md) | FatalErrorsSetup | [FatalErrorsSetup](./kibana-plugin-core-public.fatalerrorssetup.md) | | [getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md) | StartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-public.startservicesaccessor.md) | | [http](./kibana-plugin-core-public.coresetup.http.md) | HttpSetup | [HttpSetup](./kibana-plugin-core-public.httpsetup.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.handlercontexttype.md b/docs/development/core/public/kibana-plugin-core-public.handlercontexttype.md deleted file mode 100644 index c55d6e4972c08..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.handlercontexttype.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [HandlerContextType](./kibana-plugin-core-public.handlercontexttype.md) - -## HandlerContextType type - -Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-core-public.handlerfunction.md) to represent the type of the context. - -Signature: - -```typescript -export declare type HandlerContextType> = T extends HandlerFunction ? U : never; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.handlerfunction.md b/docs/development/core/public/kibana-plugin-core-public.handlerfunction.md deleted file mode 100644 index 741c1d7d6cd52..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.handlerfunction.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [HandlerFunction](./kibana-plugin-core-public.handlerfunction.md) - -## HandlerFunction type - -A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-core-public.icontextcontainer.md) - -Signature: - -```typescript -export declare type HandlerFunction = (context: T, ...args: any[]) => any; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.handlerparameters.md b/docs/development/core/public/kibana-plugin-core-public.handlerparameters.md deleted file mode 100644 index d863c7a2c7837..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.handlerparameters.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [HandlerParameters](./kibana-plugin-core-public.handlerparameters.md) - -## HandlerParameters type - -Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-core-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-core-public.handlercontexttype.md). - -Signature: - -```typescript -export declare type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.icontextcontainer.createhandler.md b/docs/development/core/public/kibana-plugin-core-public.icontextcontainer.createhandler.md deleted file mode 100644 index 4823b864ce04c..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.icontextcontainer.createhandler.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IContextContainer](./kibana-plugin-core-public.icontextcontainer.md) > [createHandler](./kibana-plugin-core-public.icontextcontainer.createhandler.md) - -## IContextContainer.createHandler() method - -Create a new handler function pre-wired to context for the plugin. - -Signature: - -```typescript -createHandler(pluginOpaqueId: PluginOpaqueId, handler: THandler): (...rest: HandlerParameters) => ShallowPromise>; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| pluginOpaqueId | PluginOpaqueId | The plugin opaque ID for the plugin that registers this handler. | -| handler | THandler | Handler function to pass context object to. | - -Returns: - -`(...rest: HandlerParameters) => ShallowPromise>` - -A function that takes `THandlerParameters`, calls `handler` with a new context, and returns a Promise of the `handler` return value. - diff --git a/docs/development/core/public/kibana-plugin-core-public.icontextcontainer.md b/docs/development/core/public/kibana-plugin-core-public.icontextcontainer.md deleted file mode 100644 index e1678931f9e21..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.icontextcontainer.md +++ /dev/null @@ -1,80 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IContextContainer](./kibana-plugin-core-public.icontextcontainer.md) - -## IContextContainer interface - -An object that handles registration of context providers and configuring handlers with context. - -Signature: - -```typescript -export interface IContextContainer> -``` - -## Remarks - -A [IContextContainer](./kibana-plugin-core-public.icontextcontainer.md) can be used by any Core service or plugin (known as the "service owner") which wishes to expose APIs in a handler function. The container object will manage registering context providers and configuring a handler with all of the contexts that should be exposed to the handler's plugin. This is dependent on the dependencies that the handler's plugin declares. - -Contexts providers are executed in the order they were registered. Each provider gets access to context values provided by any plugins that it depends on. - -In order to configure a handler with context, you must call the [IContextContainer.createHandler()](./kibana-plugin-core-public.icontextcontainer.createhandler.md) function and use the returned handler which will automatically build a context object when called. - -When registering context or creating handlers, the \_calling plugin's opaque id\_ must be provided. This id is passed in via the plugin's initializer and can be accessed from the [PluginInitializerContext.opaqueId](./kibana-plugin-core-public.plugininitializercontext.opaqueid.md) Note this should NOT be the context service owner's id, but the plugin that is actually registering the context or handler. - -```ts -// Correct -class MyPlugin { - private readonly handlers = new Map(); - - setup(core) { - this.contextContainer = core.context.createContextContainer(); - return { - registerContext(pluginOpaqueId, contextName, provider) { - this.contextContainer.registerContext(pluginOpaqueId, contextName, provider); - }, - registerRoute(pluginOpaqueId, path, handler) { - this.handlers.set( - path, - this.contextContainer.createHandler(pluginOpaqueId, handler) - ); - } - } - } -} - -// Incorrect -class MyPlugin { - private readonly handlers = new Map(); - - constructor(private readonly initContext: PluginInitializerContext) {} - - setup(core) { - this.contextContainer = core.context.createContextContainer(); - return { - registerContext(contextName, provider) { - // BUG! - // This would leak this context to all handlers rather that only plugins that depend on the calling plugin. - this.contextContainer.registerContext(this.initContext.opaqueId, contextName, provider); - }, - registerRoute(path, handler) { - this.handlers.set( - path, - // BUG! - // This handler will not receive any contexts provided by other dependencies of the calling plugin. - this.contextContainer.createHandler(this.initContext.opaqueId, handler) - ); - } - } - } -} - -``` - -## Methods - -| Method | Description | -| --- | --- | -| [createHandler(pluginOpaqueId, handler)](./kibana-plugin-core-public.icontextcontainer.createhandler.md) | Create a new handler function pre-wired to context for the plugin. | -| [registerContext(pluginOpaqueId, contextName, provider)](./kibana-plugin-core-public.icontextcontainer.registercontext.md) | Register a new context provider. | - diff --git a/docs/development/core/public/kibana-plugin-core-public.icontextcontainer.registercontext.md b/docs/development/core/public/kibana-plugin-core-public.icontextcontainer.registercontext.md deleted file mode 100644 index 2fc08fccc931d..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.icontextcontainer.registercontext.md +++ /dev/null @@ -1,34 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IContextContainer](./kibana-plugin-core-public.icontextcontainer.md) > [registerContext](./kibana-plugin-core-public.icontextcontainer.registercontext.md) - -## IContextContainer.registerContext() method - -Register a new context provider. - -Signature: - -```typescript -registerContext>(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider): this; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| pluginOpaqueId | PluginOpaqueId | The plugin opaque ID for the plugin that registers this context. | -| contextName | TContextName | The key of the TContext object this provider supplies the value for. | -| provider | IContextProvider<THandler, TContextName> | A [IContextProvider](./kibana-plugin-core-public.icontextprovider.md) to be called each time a new context is created. | - -Returns: - -`this` - -The [IContextContainer](./kibana-plugin-core-public.icontextcontainer.md) for method chaining. - -## Remarks - -The value (or resolved Promise value) returned by the `provider` function will be attached to the context object on the key specified by `contextName`. - -Throws an exception if more than one provider is registered for the same `contextName`. - diff --git a/docs/development/core/public/kibana-plugin-core-public.icontextprovider.md b/docs/development/core/public/kibana-plugin-core-public.icontextprovider.md deleted file mode 100644 index 97f7bad8e9911..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.icontextprovider.md +++ /dev/null @@ -1,18 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IContextProvider](./kibana-plugin-core-public.icontextprovider.md) - -## IContextProvider type - -A function that returns a context value for a specific key of given context type. - -Signature: - -```typescript -export declare type IContextProvider, TContextName extends keyof HandlerContextType> = (context: PartialExceptFor, 'core'>, ...rest: HandlerParameters) => Promise[TContextName]> | HandlerContextType[TContextName]; -``` - -## Remarks - -This function will be called each time a new context is built for a handler invocation. - diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index 1db3bd31bbc9b..bfe787f3793d7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -38,7 +38,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) | | | [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) | | | [AppMeta](./kibana-plugin-core-public.appmeta.md) | Input type for meta data for an application.Meta fields include keywords and searchDeepLinks Keywords is an array of string with which to associate the app, must include at least one unique string as an array. searchDeepLinks is an array of links that represent secondary in-app locations for the app. | -| [AppMountContext](./kibana-plugin-core-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md). | | [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) | | | [Capabilities](./kibana-plugin-core-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | | [ChromeBadge](./kibana-plugin-core-public.chromebadge.md) | | @@ -56,7 +55,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ChromeRecentlyAccessed](./kibana-plugin-core-public.chromerecentlyaccessed.md) | [APIs](./kibana-plugin-core-public.chromerecentlyaccessed.md) for recently accessed history. | | [ChromeRecentlyAccessedHistoryItem](./kibana-plugin-core-public.chromerecentlyaccessedhistoryitem.md) | | | [ChromeStart](./kibana-plugin-core-public.chromestart.md) | ChromeStart allows plugins to customize the global chrome header UI and enrich the UX with additional information about the current location of the browser. | -| [ContextSetup](./kibana-plugin-core-public.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. | | [CoreSetup](./kibana-plugin-core-public.coresetup.md) | Core services exposed to the Plugin setup lifecycle | | [CoreStart](./kibana-plugin-core-public.corestart.md) | Core services exposed to the Plugin start lifecycle | | [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) | | @@ -77,7 +75,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [I18nStart](./kibana-plugin-core-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. | | [IAnonymousPaths](./kibana-plugin-core-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication | | [IBasePath](./kibana-plugin-core-public.ibasepath.md) | APIs for manipulating the basePath on URL segments. | -| [IContextContainer](./kibana-plugin-core-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | | [IExternalUrl](./kibana-plugin-core-public.iexternalurl.md) | APIs for working with external URLs. | | [IExternalUrlPolicy](./kibana-plugin-core-public.iexternalurlpolicy.md) | A policy describing whether access to an external destination is allowed. | | [IHttpFetchError](./kibana-plugin-core-public.ihttpfetcherror.md) | | @@ -144,7 +141,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppLeaveAction](./kibana-plugin-core-public.appleaveaction.md) | Possible actions to return from a [AppLeaveHandler](./kibana-plugin-core-public.appleavehandler.md)See [AppLeaveConfirmAction](./kibana-plugin-core-public.appleaveconfirmaction.md) and [AppLeaveDefaultAction](./kibana-plugin-core-public.appleavedefaultaction.md) | | [AppLeaveHandler](./kibana-plugin-core-public.appleavehandler.md) | A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return confirm to to prompt a message to the user before leaving the page, or default to keep the default behavior (doing nothing).See [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) for detailed usage examples. | | [AppMount](./kibana-plugin-core-public.appmount.md) | A mount function called when the user navigates to this app's route. | -| [AppMountDeprecated](./kibana-plugin-core-public.appmountdeprecated.md) | A mount function called when the user navigates to this app's route. | | [AppSearchDeepLink](./kibana-plugin-core-public.appsearchdeeplink.md) | Input type for registering secondary in-app locations for an application.Deep links must include at least one of path or searchDeepLinks. A deep link that does not have a path represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. | | [AppUnmount](./kibana-plugin-core-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. | | [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) | Defines the list of fields that can be updated via an [AppUpdater](./kibana-plugin-core-public.appupdater.md). | @@ -154,11 +150,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ChromeHelpExtensionMenuLink](./kibana-plugin-core-public.chromehelpextensionmenulink.md) | | | [ChromeNavLinkUpdateableFields](./kibana-plugin-core-public.chromenavlinkupdateablefields.md) | | | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | -| [HandlerContextType](./kibana-plugin-core-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-core-public.handlerfunction.md) to represent the type of the context. | -| [HandlerFunction](./kibana-plugin-core-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-core-public.icontextcontainer.md) | -| [HandlerParameters](./kibana-plugin-core-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-core-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-core-public.handlercontexttype.md). | | [HttpStart](./kibana-plugin-core-public.httpstart.md) | See [HttpSetup](./kibana-plugin-core-public.httpsetup.md) | -| [IContextProvider](./kibana-plugin-core-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | | [IToasts](./kibana-plugin-core-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-core-public.toastsapi.md). | | [MountPoint](./kibana-plugin-core-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. | | [NavType](./kibana-plugin-core-public.navtype.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 75227575038ab..1b4cf5585cb3e 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -14,7 +14,7 @@ | [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | | -| [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) | The OSS session service. See data\_enhanced in X-Pack for the background session service. | +| [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) | The OSS session service. See data\_enhanced in X-Pack for the search session service. | ## Enumerations diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.md index 2369b00644548..2457f7103d8fa 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.md @@ -4,7 +4,7 @@ ## SessionService class -The OSS session service. See data\_enhanced in X-Pack for the background session service. +The OSS session service. See data\_enhanced in X-Pack for the search session service. Signature: diff --git a/examples/embeddable_explorer/public/app.tsx b/examples/embeddable_explorer/public/app.tsx index 7fb035f1f279a..708f5e946fdd6 100644 --- a/examples/embeddable_explorer/public/app.tsx +++ b/examples/embeddable_explorer/public/app.tsx @@ -27,7 +27,6 @@ import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; import { Start as InspectorStartContract } from '../../../src/plugins/inspector/public'; import { - AppMountContext, AppMountParameters, CoreStart, SavedObjectsStart, @@ -47,7 +46,7 @@ interface PageDef { } type NavProps = RouteComponentProps & { - navigateToApp: AppMountContext['core']['application']['navigateToApp']; + navigateToApp: CoreStart['application']['navigateToApp']; pages: PageDef[]; }; diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts index 5609e7eb5d17d..16bb6885ac3f3 100644 --- a/src/core/public/application/application_service.mock.ts +++ b/src/core/public/application/application_service.mock.ts @@ -34,13 +34,11 @@ import { ApplicationServiceContract } from './test_types'; const createSetupContractMock = (): jest.Mocked => ({ register: jest.fn(), registerAppUpdater: jest.fn(), - registerMountContext: jest.fn(), }); const createInternalSetupContractMock = (): jest.Mocked => ({ register: jest.fn(), registerAppUpdater: jest.fn(), - registerMountContext: jest.fn(), }); const createStartContractMock = (): jest.Mocked => { @@ -53,7 +51,6 @@ const createStartContractMock = (): jest.Mocked => { navigateToApp: jest.fn(), navigateToUrl: jest.fn(), getUrlForApp: jest.fn(), - registerMountContext: jest.fn(), }; }; @@ -91,7 +88,6 @@ const createInternalStartContractMock = (): jest.Mocked currentAppId$.next(appId)), navigateToUrl: jest.fn(), - registerMountContext: jest.fn(), history: createHistoryMock(), }; }; diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 912ab40cbe1db..f9e5c6112919a 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -28,7 +28,6 @@ import { BehaviorSubject, Subject } from 'rxjs'; import { bufferCount, take, takeUntil } from 'rxjs/operators'; import { shallow, mount } from 'enzyme'; -import { contextServiceMock } from '../context/context_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; import { overlayServiceMock } from '../overlays/overlay_service.mock'; import { MockLifecycle } from './test_types'; @@ -54,7 +53,6 @@ describe('#setup()', () => { const http = httpServiceMock.createSetupContract({ basePath: '/base-path' }); setupDeps = { http, - context: contextServiceMock.createSetupContract(), redirectTo: jest.fn(), }; startDeps = { http, overlays: overlayServiceMock.createStartContract() }; @@ -367,16 +365,6 @@ describe('#setup()', () => { MockHistory.push.mockClear(); }); }); - - it("`registerMountContext` calls context container's registerContext", () => { - const { registerMountContext } = service.setup(setupDeps); - const container = setupDeps.context.createContextContainer.mock.results[0].value; - const pluginId = Symbol(); - - const appMount = () => () => undefined; - registerMountContext(pluginId, 'test' as any, appMount); - expect(container.registerContext).toHaveBeenCalledWith(pluginId, 'test', appMount); - }); }); describe('#start()', () => { @@ -384,7 +372,6 @@ describe('#start()', () => { const http = httpServiceMock.createSetupContract({ basePath: '/base-path' }); setupDeps = { http, - context: contextServiceMock.createSetupContract(), redirectTo: jest.fn(), }; startDeps = { http, overlays: overlayServiceMock.createStartContract() }; @@ -869,7 +856,6 @@ describe('#stop()', () => { const http = httpServiceMock.createSetupContract({ basePath: '/test' }); setupDeps = { http, - context: contextServiceMock.createSetupContract(), }; startDeps = { http, overlays: overlayServiceMock.createStartContract() }; service = new ApplicationService(); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 67281170957c6..cec600f9e3d1f 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -25,7 +25,6 @@ import { createBrowserHistory, History } from 'history'; import { MountPoint } from '../types'; import { HttpSetup, HttpStart } from '../http'; import { OverlayStart } from '../overlays'; -import { ContextSetup, IContextContainer } from '../context'; import { PluginOpaqueId } from '../plugins'; import { AppRouter } from './ui'; import { Capabilities, CapabilitiesService } from './capabilities'; @@ -33,7 +32,6 @@ import { App, AppLeaveHandler, AppMount, - AppMountDeprecated, AppNavLinkStatus, AppStatus, AppUpdatableFields, @@ -47,7 +45,6 @@ import { getLeaveAction, isConfirmAction } from './application_leave'; import { appendAppPath, parseAppUrl, relativeToAbsolute, getAppInfo } from './utils'; interface SetupDeps { - context: ContextSetup; http: HttpSetup; history?: History; /** Used to redirect to external urls */ @@ -59,9 +56,6 @@ interface StartDeps { overlays: OverlayStart; } -// Mount functions with two arguments are assumed to expect deprecated `context` object. -const isAppMountDeprecated = (mount: (...args: any[]) => any): mount is AppMountDeprecated => - mount.length === 2; function filterAvailable(m: Map, capabilities: Capabilities) { return new Map( [...m].filter( @@ -107,12 +101,10 @@ export class ApplicationService { private stop$ = new Subject(); private registrationClosed = false; private history?: History; - private mountContext?: IContextContainer; private navigate?: (url: string, state: unknown, replace: boolean) => void; private redirectTo?: (url: string) => void; public setup({ - context, http: { basePath }, redirectTo = (path: string) => { window.location.assign(path); @@ -128,7 +120,6 @@ export class ApplicationService { }; this.redirectTo = redirectTo; - this.mountContext = context.createContextContainer(); const registerStatusUpdater = (application: string, updater$: Observable) => { const updaterId = Symbol(); @@ -144,26 +135,13 @@ export class ApplicationService { }; const wrapMount = (plugin: PluginOpaqueId, app: App): AppMount => { - let handler: AppMount; - if (isAppMountDeprecated(app.mount)) { - handler = this.mountContext!.createHandler(plugin, app.mount); - if (process.env.NODE_ENV === 'development') { - // eslint-disable-next-line no-console - console.warn( - `App [${app.id}] is using deprecated mount context. Use core.getStartServices() instead.` - ); - } - } else { - handler = app.mount; - } return async (params) => { this.currentAppId$.next(app.id); - return handler(params); + return app.mount(params); }; }; return { - registerMountContext: this.mountContext!.registerContext, register: (plugin, app: App) => { app = { appRoute: `/app/${app.id}`, ...app }; @@ -202,7 +180,7 @@ export class ApplicationService { } public async start({ http, overlays }: StartDeps): Promise { - if (!this.mountContext) { + if (!this.redirectTo) { throw new Error('ApplicationService#setup() must be invoked before start.'); } @@ -278,7 +256,6 @@ export class ApplicationService { takeUntil(this.stop$) ), history: this.history!, - registerMountContext: this.mountContext.registerContext, getUrlForApp: ( appId, { path, absolute = false }: { path?: string; absolute?: boolean } = {} diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index 96d669a7b96f4..e865c73e4e217 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -23,9 +23,7 @@ export { ScopedHistory } from './scoped_history'; export { App, AppMount, - AppMountDeprecated, AppUnmount, - AppMountContext, AppMountParameters, AppStatus, AppNavLinkStatus, diff --git a/src/core/public/application/integration_tests/application_service.test.tsx b/src/core/public/application/integration_tests/application_service.test.tsx index 2ccb8ec64f910..2e929a853128a 100644 --- a/src/core/public/application/integration_tests/application_service.test.tsx +++ b/src/core/public/application/integration_tests/application_service.test.tsx @@ -24,7 +24,6 @@ import { createMemoryHistory, MemoryHistory } from 'history'; import { createRenderer } from './utils'; import { ApplicationService } from '../application_service'; import { httpServiceMock } from '../../http/http_service.mock'; -import { contextServiceMock } from '../../context/context_service.mock'; import { MockLifecycle } from '../test_types'; import { overlayServiceMock } from '../../overlays/overlay_service.mock'; import { AppMountParameters } from '../types'; @@ -53,7 +52,6 @@ describe('ApplicationService', () => { setupDeps = { http, - context: contextServiceMock.createSetupContract(), history: history as any, }; startDeps = { http, overlays: overlayServiceMock.createStartContract() }; diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 0a31490ad664c..5507ab3eb56c0 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -23,16 +23,7 @@ import { RecursiveReadonly } from '@kbn/utility-types'; import { MountPoint } from '../types'; import { Capabilities } from './capabilities'; -import { ChromeStart } from '../chrome'; -import { IContextProvider } from '../context'; -import { DocLinksStart } from '../doc_links'; -import { HttpStart } from '../http'; -import { I18nStart } from '../i18n'; -import { NotificationsStart } from '../notifications'; -import { OverlayStart } from '../overlays'; import { PluginOpaqueId } from '../plugins'; -import { IUiSettingsClient } from '../ui_settings'; -import { SavedObjectsStart } from '../saved_objects'; import { AppCategory } from '../../types'; import { ScopedHistory } from './scoped_history'; @@ -202,14 +193,9 @@ export interface App { chromeless?: boolean; /** - * A mount function called when the user navigates to this app's route. May have signature of {@link AppMount} or - * {@link AppMountDeprecated}. - * - * @remarks - * When function has two arguments, it will be called with a {@link AppMountContext | context} as the first argument. - * This behavior is **deprecated**, and consumers should instead use {@link CoreSetup.getStartServices}. + * A mount function called when the user navigates to this app's route. */ - mount: AppMount | AppMountDeprecated; + mount: AppMount; /** * Override the application's routing path from `/app/${id}`. @@ -370,67 +356,6 @@ export type AppMount = ( */ export type AppUnmount = () => void; -/** - * A mount function called when the user navigates to this app's route. - * - * @remarks - * When function has two arguments, it will be called with a {@link AppMountContext | context} as the first argument. - * This behavior is **deprecated**, and consumers should instead use {@link CoreSetup.getStartServices}. - * - * @param context The mount context for this app. Deprecated, use {@link CoreSetup.getStartServices}. - * @param params {@link AppMountParameters} - * @returns An unmounting function that will be called to unmount the application. See {@link AppUnmount}. - * - * @deprecated - * @public - */ -export type AppMountDeprecated = ( - context: AppMountContext, - params: AppMountParameters -) => AppUnmount | Promise; - -/** - * The context object received when applications are mounted to the DOM. Deprecated, use - * {@link CoreSetup.getStartServices}. - * - * @deprecated - * @public - */ -export interface AppMountContext { - /** - * Core service APIs available to mounted applications. - */ - core: { - /** {@link ApplicationStart} */ - application: Pick; - /** {@link ChromeStart} */ - chrome: ChromeStart; - /** {@link DocLinksStart} */ - docLinks: DocLinksStart; - /** {@link HttpStart} */ - http: HttpStart; - /** {@link I18nStart} */ - i18n: I18nStart; - /** {@link NotificationsStart} */ - notifications: NotificationsStart; - /** {@link OverlayStart} */ - overlays: OverlayStart; - /** {@link SavedObjectsStart} */ - savedObjects: SavedObjectsStart; - /** {@link IUiSettingsClient} */ - uiSettings: IUiSettingsClient; - /** - * exposed temporarily until https://github.com/elastic/kibana/issues/41990 done - * use *only* to retrieve config values. There is no way to set injected values - * in the new platform. Use the legacy platform API instead. - * @deprecated - * */ - injectedMetadata: { - getInjectedVar: (name: string, defaultValue?: any) => unknown; - }; - }; -} - /** @public */ export interface AppMountParameters { /** @@ -735,19 +660,6 @@ export interface ApplicationSetup { * ``` */ registerAppUpdater(appUpdater$: Observable): void; - - /** - * Register a context provider for application mounting. Will only be available to applications that depend on the - * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. - * - * @deprecated - * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. - * @param provider - A {@link IContextProvider} function - */ - registerMountContext( - contextName: T, - provider: IContextProvider - ): void; } /** @internal */ @@ -761,21 +673,6 @@ export interface InternalApplicationSetup extends Pick ): void; - - /** - * Register a context provider for application mounting. Will only be available to applications that depend on the - * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. - * - * @deprecated - * @param pluginOpaqueId - The opaque ID of the plugin that is registering the context. - * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. - * @param provider - A {@link IContextProvider} function - */ - registerMountContext( - pluginOpaqueId: PluginOpaqueId, - contextName: T, - provider: IContextProvider - ): void; } /** @@ -873,19 +770,6 @@ export interface ApplicationStart { */ getUrlForApp(appId: string, options?: { path?: string; absolute?: boolean }): string; - /** - * Register a context provider for application mounting. Will only be available to applications that depend on the - * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. - * - * @deprecated - * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. - * @param provider - A {@link IContextProvider} function - */ - registerMountContext( - contextName: T, - provider: IContextProvider - ): void; - /** * An observable that emits the current application id and each subsequent id update. */ @@ -893,22 +777,7 @@ export interface ApplicationStart { } /** @internal */ -export interface InternalApplicationStart extends Omit { - /** - * Register a context provider for application mounting. Will only be available to applications that depend on the - * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. - * - * @deprecated - * @param pluginOpaqueId - The opaque ID of the plugin that is registering the context. - * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. - * @param provider - A {@link IContextProvider} function - */ - registerMountContext( - pluginOpaqueId: PluginOpaqueId, - contextName: T, - provider: IContextProvider - ): void; - +export interface InternalApplicationStart extends ApplicationStart { // Internal APIs getComponent(): JSX.Element | null; diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index dd07dbd9eaee2..52349465eca40 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -186,7 +186,6 @@ exports[`Header renders 1`] = ` }, "navigateToApp": [MockFunction], "navigateToUrl": [MockFunction], - "registerMountContext": [MockFunction], } } badge$={ diff --git a/src/core/public/context/context_service.mock.ts b/src/core/public/context/context_service.mock.ts deleted file mode 100644 index 3f9f647e1f7d0..0000000000000 --- a/src/core/public/context/context_service.mock.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import type { PublicMethodsOf } from '@kbn/utility-types'; -import { ContextService, ContextSetup } from './context_service'; -import { contextMock } from '../../utils/context.mock'; - -const createSetupContractMock = () => { - const setupContract: jest.Mocked = { - createContextContainer: jest.fn().mockImplementation(() => contextMock.create()), - }; - return setupContract; -}; - -type ContextServiceContract = PublicMethodsOf; -const createMock = () => { - const mocked: jest.Mocked = { - setup: jest.fn(), - }; - mocked.setup.mockReturnValue(createSetupContractMock()); - return mocked; -}; - -export const contextServiceMock = { - create: createMock, - createSetupContract: createSetupContractMock, -}; diff --git a/src/core/public/context/context_service.test.mocks.ts b/src/core/public/context/context_service.test.mocks.ts deleted file mode 100644 index 5cf492d97aaf2..0000000000000 --- a/src/core/public/context/context_service.test.mocks.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { contextMock } from '../../utils/context.mock'; - -export const MockContextConstructor = jest.fn(contextMock.create); -jest.doMock('../../utils/context', () => ({ - ContextContainer: MockContextConstructor, -})); diff --git a/src/core/public/context/context_service.test.ts b/src/core/public/context/context_service.test.ts deleted file mode 100644 index 934ea77df6fdc..0000000000000 --- a/src/core/public/context/context_service.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { PluginOpaqueId } from '../../server'; -import { MockContextConstructor } from './context_service.test.mocks'; -import { ContextService } from './context_service'; -import { coreMock } from '../mocks'; - -const pluginDependencies = new Map(); - -describe('ContextService', () => { - describe('#setup()', () => { - test('createContextContainer returns a new container configured with pluginDependencies', () => { - const context = coreMock.createCoreContext(); - const service = new ContextService(context); - const setup = service.setup({ pluginDependencies }); - expect(setup.createContextContainer()).toBeDefined(); - expect(MockContextConstructor).toHaveBeenCalledWith(pluginDependencies, context.coreId); - }); - }); -}); diff --git a/src/core/public/context/context_service.ts b/src/core/public/context/context_service.ts deleted file mode 100644 index 7860b486da959..0000000000000 --- a/src/core/public/context/context_service.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { PluginOpaqueId } from '../../server'; -import { IContextContainer, ContextContainer, HandlerFunction } from '../../utils/context'; -import { CoreContext } from '../core_system'; - -interface StartDeps { - pluginDependencies: ReadonlyMap; -} - -/** @internal */ -export class ContextService { - constructor(private readonly core: CoreContext) {} - - public setup({ pluginDependencies }: StartDeps): ContextSetup { - return { - createContextContainer: >() => - new ContextContainer(pluginDependencies, this.core.coreId), - }; - } -} - -/** - * {@inheritdoc IContextContainer} - * - * @example - * Say we're creating a plugin for rendering visualizations that allows new rendering methods to be registered. If we - * want to offer context to these rendering methods, we can leverage the ContextService to manage these contexts. - * ```ts - * export interface VizRenderContext { - * core: { - * i18n: I18nStart; - * uiSettings: IUiSettingsClient; - * } - * [contextName: string]: unknown; - * } - * - * export type VizRenderer = (context: VizRenderContext, domElement: HTMLElement) => () => void; - * // When a renderer is bound via `contextContainer.createHandler` this is the type that will be returned. - * type BoundVizRenderer = (domElement: HTMLElement) => () => void; - * - * class VizRenderingPlugin { - * private readonly contextContainer?: IContextContainer; - * private readonly vizRenderers = new Map(); - * - * constructor(private readonly initContext: PluginInitializerContext) {} - * - * setup(core) { - * this.contextContainer = core.context.createContextContainer(); - * - * return { - * registerContext: this.contextContainer.registerContext, - * registerVizRenderer: (plugin: PluginOpaqueId, renderMethod: string, renderer: VizTypeRenderer) => - * this.vizRenderers.set(renderMethod, this.contextContainer.createHandler(plugin, renderer)), - * }; - * } - * - * start(core) { - * // Register the core context available to all renderers. Use the VizRendererContext's opaqueId as the first arg. - * this.contextContainer.registerContext(this.initContext.opaqueId, 'core', () => ({ - * i18n: core.i18n, - * uiSettings: core.uiSettings - * })); - * - * return { - * registerContext: this.contextContainer.registerContext, - * - * renderVizualization: (renderMethod: string, domElement: HTMLElement) => { - * if (!this.vizRenderer.has(renderMethod)) { - * throw new Error(`Render method '${renderMethod}' has not been registered`); - * } - * - * // The handler can now be called directly with only an `HTMLElement` and will automatically - * // have a new `context` object created and populated by the context container. - * const handler = this.vizRenderers.get(renderMethod) - * return handler(domElement); - * } - * }; - * } - * } - * ``` - * - * @public - */ -export interface ContextSetup { - /** - * Creates a new {@link IContextContainer} for a service owner. - */ - createContextContainer>(): IContextContainer; -} diff --git a/src/core/public/context/index.ts b/src/core/public/context/index.ts deleted file mode 100644 index f22c4168d7544..0000000000000 --- a/src/core/public/context/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { ContextService, ContextSetup } from './context_service'; -export { - IContextContainer, - IContextProvider, - HandlerFunction, - HandlerContextType, - HandlerParameters, -} from '../../utils/context'; diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index d0e457386ffca..27c05a9e68737 100644 --- a/src/core/public/core_system.test.mocks.ts +++ b/src/core/public/core_system.test.mocks.ts @@ -29,7 +29,6 @@ import { pluginsServiceMock } from './plugins/plugins_service.mock'; import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { docLinksServiceMock } from './doc_links/doc_links_service.mock'; import { renderingServiceMock } from './rendering/rendering_service.mock'; -import { contextServiceMock } from './context/context_service.mock'; import { integrationsServiceMock } from './integrations/integrations_service.mock'; import { coreAppMock } from './core_app/core_app.mock'; @@ -115,12 +114,6 @@ jest.doMock('./rendering', () => ({ RenderingService: RenderingServiceConstructor, })); -export const MockContextService = contextServiceMock.create(); -export const ContextServiceConstructor = jest.fn().mockImplementation(() => MockContextService); -jest.doMock('./context', () => ({ - ContextService: ContextServiceConstructor, -})); - export const MockIntegrationsService = integrationsServiceMock.create(); export const IntegrationsServiceConstructor = jest .fn() diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 213237309c30b..a5d45d4ad5692 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -39,7 +39,6 @@ import { MockDocLinksService, MockRenderingService, RenderingServiceConstructor, - MockContextService, IntegrationsServiceConstructor, MockIntegrationsService, CoreAppConstructor, @@ -149,29 +148,6 @@ describe('#setup()', () => { expect(MockApplicationService.setup).toHaveBeenCalledTimes(1); }); - it('calls context#setup()', async () => { - await setupCore(); - expect(MockContextService.setup).toHaveBeenCalledTimes(1); - }); - - it('injects legacy dependency to context#setup()', async () => { - const pluginA = Symbol(); - const pluginB = Symbol(); - const pluginDependencies = new Map([ - [pluginA, []], - [pluginB, [pluginA]], - ]); - MockPluginsService.getOpaqueIds.mockReturnValue(pluginDependencies); - await setupCore(); - - expect(MockContextService.setup).toHaveBeenCalledWith({ - pluginDependencies: new Map([ - [pluginA, []], - [pluginB, [pluginA]], - ]), - }); - }); - it('calls injectedMetadata#setup()', async () => { await setupCore(); expect(MockInjectedMetadataService.setup).toHaveBeenCalledTimes(1); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index a1d6f9c988b27..c1e3a21c97740 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -17,7 +17,6 @@ * under the License. */ -import { pick } from '@kbn/std'; import { CoreId } from '../server'; import { PackageInfo, EnvironmentMode } from '../server/types'; import { CoreSetup, CoreStart } from '.'; @@ -39,7 +38,6 @@ import { ApplicationService } from './application'; import { DocLinksService } from './doc_links'; import { RenderingService } from './rendering'; import { SavedObjectsService } from './saved_objects'; -import { ContextService } from './context'; import { IntegrationsService } from './integrations'; import { CoreApp } from './core_app'; import type { InternalApplicationSetup, InternalApplicationStart } from './application/types'; @@ -93,7 +91,6 @@ export class CoreSystem { private readonly application: ApplicationService; private readonly docLinks: DocLinksService; private readonly rendering: RenderingService; - private readonly context: ContextService; private readonly integrations: IntegrationsService; private readonly coreApp: CoreApp; @@ -129,8 +126,6 @@ export class CoreSystem { this.integrations = new IntegrationsService(); this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env }; - - this.context = new ContextService(this.coreContext); this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); this.coreApp = new CoreApp(this.coreContext); } @@ -150,16 +145,11 @@ export class CoreSystem { const uiSettings = this.uiSettings.setup({ http, injectedMetadata }); const notifications = this.notifications.setup({ uiSettings }); - const pluginDependencies = this.plugins.getOpaqueIds(); - const context = this.context.setup({ - pluginDependencies: new Map([...pluginDependencies]), - }); - const application = this.application.setup({ context, http }); + const application = this.application.setup({ http }); this.coreApp.setup({ application, http, injectedMetadata, notifications }); const core: InternalCoreSetup = { application, - context, fatalErrors: this.fatalErrorsSetup, http, injectedMetadata, @@ -220,19 +210,6 @@ export class CoreSystem { this.coreApp.start({ application, http, notifications, uiSettings }); - application.registerMountContext(this.coreContext.coreId, 'core', () => ({ - application: pick(application, ['capabilities', 'navigateToApp']), - chrome, - docLinks, - http, - i18n, - injectedMetadata: pick(injectedMetadata, ['getInjectedVar']), - notifications, - overlays, - savedObjects, - uiSettings, - })); - const core: InternalCoreStart = { application, chrome, diff --git a/src/core/public/index.ts b/src/core/public/index.ts index ea83674ed9d9c..458a97969b88a 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -69,14 +69,6 @@ import { UiSettingsState, IUiSettingsClient } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; import { DocLinksStart } from './doc_links'; import { SavedObjectsStart } from './saved_objects'; -import { - IContextContainer, - IContextProvider, - ContextSetup, - HandlerFunction, - HandlerContextType, - HandlerParameters, -} from './context'; export { PackageInfo, EnvironmentMode, IExternalUrlPolicy } from '../server/types'; export { CoreContext, CoreSystem } from './core_system'; @@ -97,9 +89,7 @@ export { ApplicationStart, App, AppMount, - AppMountDeprecated, AppUnmount, - AppMountContext, AppMountParameters, AppLeaveHandler, AppLeaveActionType, @@ -217,11 +207,6 @@ export { URL_MAX_LENGTH } from './core_app'; export interface CoreSetup { /** {@link ApplicationSetup} */ application: ApplicationSetup; - /** - * {@link ContextSetup} - * @deprecated - */ - context: ContextSetup; /** {@link FatalErrorsSetup} */ fatalErrors: FatalErrorsSetup; /** {@link HttpSetup} */ @@ -317,12 +302,6 @@ export { ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem, ChromeStart, - IContextContainer, - HandlerFunction, - HandlerContextType, - HandlerParameters, - IContextProvider, - ContextSetup, DocLinksStart, FatalErrorInfo, FatalErrorsSetup, diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 1263d160f6b78..157d1a44355dd 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -33,7 +33,6 @@ import { notificationServiceMock } from './notifications/notifications_service.m import { overlayServiceMock } from './overlays/overlay_service.mock'; import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; -import { contextServiceMock } from './context/context_service.mock'; import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock'; export { chromeServiceMock } from './chrome/chrome_service.mock'; @@ -60,7 +59,6 @@ function createCoreSetupMock({ } = {}) { const mock = { application: applicationServiceMock.createSetupContract(), - context: contextServiceMock.createSetupContract(), docLinks: docLinksServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), getStartServices: jest.fn, any, any]>, []>(() => diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 1eb745fd80437..9b86a50872e40 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -97,10 +97,7 @@ export function createPluginSetupContext< application: { register: (app) => deps.application.register(plugin.opaqueId, app), registerAppUpdater: (statusUpdater$) => deps.application.registerAppUpdater(statusUpdater$), - registerMountContext: (contextName, provider) => - deps.application.registerMountContext(plugin.opaqueId, contextName, provider), }, - context: deps.context, fatalErrors: deps.fatalErrors, http: deps.http, notifications: deps.notifications, @@ -140,8 +137,6 @@ export function createPluginStartContext< navigateToApp: deps.application.navigateToApp, navigateToUrl: deps.application.navigateToUrl, getUrlForApp: deps.application.getUrlForApp, - registerMountContext: (contextName, provider) => - deps.application.registerMountContext(plugin.opaqueId, contextName, provider), }, docLinks: deps.docLinks, http: deps.http, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 28cbfce3fd651..27178dbd5887a 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -45,7 +45,6 @@ import { httpServiceMock } from '../http/http_service.mock'; import { CoreSetup, CoreStart, PluginInitializerContext } from '..'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; -import { contextServiceMock } from '../context/context_service.mock'; export let mockPluginInitializers: Map; @@ -89,7 +88,6 @@ describe('PluginsService', () => { ]; mockSetupDeps = { application: applicationServiceMock.createInternalSetupContract(), - context: contextServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), injectedMetadata: injectedMetadataServiceMock.createStartContract(), diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 2f4c871c33431..895f0125a250c 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -32,7 +32,6 @@ import { PublicUiSettingsParams as PublicUiSettingsParams_2 } from 'src/core/ser import React from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; import * as Rx from 'rxjs'; -import { ShallowPromise } from '@kbn/utility-types'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; @@ -57,7 +56,7 @@ export interface App { icon?: string; id: string; meta?: AppMeta; - mount: AppMount | AppMountDeprecated; + mount: AppMount; navLinkStatus?: AppNavLinkStatus; order?: number; status?: AppStatus; @@ -117,8 +116,6 @@ export type AppLeaveHandler = (factory: AppLeaveActionFactory, nextAppId?: strin export interface ApplicationSetup { register(app: App): void; registerAppUpdater(appUpdater$: Observable): void; - // @deprecated - registerMountContext(contextName: T, provider: IContextProvider): void; } // @public (undocumented) @@ -132,8 +129,6 @@ export interface ApplicationStart { }): string; navigateToApp(appId: string, options?: NavigateToAppOptions): Promise; navigateToUrl(url: string): Promise; - // @deprecated - registerMountContext(contextName: T, provider: IContextProvider): void; } // @public @@ -145,27 +140,6 @@ export interface AppMeta { // @public export type AppMount = (params: AppMountParameters) => AppUnmount | Promise; -// @public @deprecated -export interface AppMountContext { - core: { - application: Pick; - chrome: ChromeStart; - docLinks: DocLinksStart; - http: HttpStart; - i18n: I18nStart; - notifications: NotificationsStart; - overlays: OverlayStart; - savedObjects: SavedObjectsStart; - uiSettings: IUiSettingsClient; - injectedMetadata: { - getInjectedVar: (name: string, defaultValue?: any) => unknown; - }; - }; -} - -// @public @deprecated -export type AppMountDeprecated = (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; - // @public (undocumented) export interface AppMountParameters { // @deprecated @@ -392,11 +366,6 @@ export interface ChromeStart { setIsVisible(isVisible: boolean): void; } -// @public -export interface ContextSetup { - createContextContainer>(): IContextContainer; -} - // @internal (undocumented) export interface CoreContext { // Warning: (ae-forgotten-export) The symbol "CoreId" needs to be exported by the entry point index.d.ts @@ -414,8 +383,6 @@ export interface CoreContext { export interface CoreSetup { // (undocumented) application: ApplicationSetup; - // @deprecated (undocumented) - context: ContextSetup; // (undocumented) fatalErrors: FatalErrorsSetup; // (undocumented) @@ -649,15 +616,6 @@ export interface FatalErrorsSetup { // @public export type FatalErrorsStart = FatalErrorsSetup; -// @public -export type HandlerContextType> = T extends HandlerFunction ? U : never; - -// @public -export type HandlerFunction = (context: T, ...args: any[]) => any; - -// @public -export type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; - // @internal (undocumented) export class HttpFetchError extends Error implements IHttpFetchError { constructor(message: string, name: string, request: Request, response?: Response | undefined, body?: any); @@ -814,17 +772,6 @@ export interface IBasePath { readonly serverBasePath: string; } -// @public -export interface IContextContainer> { - createHandler(pluginOpaqueId: PluginOpaqueId, handler: THandler): (...rest: HandlerParameters) => ShallowPromise>; - registerContext>(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider): this; -} - -// Warning: (ae-forgotten-export) The symbol "PartialExceptFor" needs to be exported by the entry point index.d.ts -// -// @public -export type IContextProvider, TContextName extends keyof HandlerContextType> = (context: PartialExceptFor, 'core'>, ...rest: HandlerParameters) => Promise[TContextName]> | HandlerContextType[TContextName]; - // @public export interface IExternalUrl { validateUrl(relativeOrAbsoluteUrl: string): URL | null; @@ -1555,6 +1502,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // -// src/core/public/core_system.ts:185:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts +// src/core/public/core_system.ts:175:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts ``` diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 386f1b369bef8..62c256d44049d 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -48,7 +48,6 @@ import * as Rx from 'rxjs'; import { SavedObjectAttributes } from 'kibana/server'; import { SavedObjectAttributes as SavedObjectAttributes_2 } from 'src/core/public'; import { SavedObjectAttributes as SavedObjectAttributes_3 } from 'kibana/public'; -import { ShallowPromise } from '@kbn/utility-types'; import { SimpleSavedObject as SimpleSavedObject_2 } from 'src/core/public'; import { Start as Start_2 } from 'src/plugins/inspector/public'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index 518b1831abda8..fdd423e10a117 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -108,7 +108,6 @@ exports[`SavedObjectsTable should render normally 1`] = ` "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "navigateToUrl": [MockFunction], - "registerMountContext": [MockFunction], } } > diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/app.tsx b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/app.tsx index f47a7c3a256f0..fff277b172f45 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/app.tsx +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app/app.tsx @@ -19,10 +19,10 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { AppMountContext, AppMountParameters } from 'kibana/public'; +import { AppMountParameters } from 'kibana/public'; import { Main } from './components/main'; -export const renderApp = (context: AppMountContext, { element }: AppMountParameters) => { +export const renderApp = ({ element }: AppMountParameters) => { render(
, element); return () => unmountComponentAtNode(element); }; diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/plugin.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/plugin.ts index 348ba215930b0..0206e375933b4 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/plugin.ts +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/plugin.ts @@ -32,9 +32,9 @@ export class Plugin { application.register({ id: 'kbn_tp_run_pipeline', title: 'Run Pipeline', - async mount(context, params) { + async mount(params) { const { renderApp } = await import('./app/app'); - return renderApp(context, params); + return renderApp(params); }, }); } diff --git a/test/plugin_functional/plugins/core_plugin_a/public/application.tsx b/test/plugin_functional/plugins/core_plugin_a/public/application.tsx index 9cf81d27e4e8d..4e9359d67f168 100644 --- a/test/plugin_functional/plugins/core_plugin_a/public/application.tsx +++ b/test/plugin_functional/plugins/core_plugin_a/public/application.tsx @@ -36,7 +36,7 @@ import { EuiSideNav, } from '@elastic/eui'; -import { AppMountContext, AppMountParameters } from 'kibana/public'; +import { CoreStart, AppMountParameters } from 'kibana/public'; const Home = () => ( @@ -83,7 +83,7 @@ const PageA = () => ( ); type NavProps = RouteComponentProps & { - navigateToApp: AppMountContext['core']['application']['navigateToApp']; + navigateToApp: CoreStart['application']['navigateToApp']; }; const Nav = withRouter(({ history, navigateToApp }: NavProps) => ( ( /> )); -const FooApp = ({ history, context }: { history: History; context: AppMountContext }) => ( +const FooApp = ({ history, coreStart }: { history: History; coreStart: CoreStart }) => ( -